Dialog
A popup that opens on top of the entire page. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Open Dialog
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<Form className="contents">
<DialogPanel className="grid gap-4">
<Field>
<FieldLabel>Name</FieldLabel>
<Input defaultValue="Margaret Welsh" type="text" />
</Field>
<Field>
<FieldLabel>Username</FieldLabel>
<Input defaultValue="@maggie.welsh" type="text" />
</Field>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>
Cancel
</DialogClose>
<Button type="submit">Save</Button>
</DialogFooter>
</Form>
</DialogPopup>
</Dialog>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/dialog
Usage
import {
Dialog,
DialogDescription,
DialogPanel,
DialogFooter,
DialogHeader,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"<Dialog>
<DialogTrigger>Open Dialog</DialogTrigger>
<DialogPopup>
<DialogHeader>
<DialogTitle>Dialog Title</DialogTitle>
<DialogDescription>Dialog Description</DialogDescription>
</DialogHeader>
<DialogPanel>Content</DialogPanel>
<DialogFooter>
<DialogClose>Close</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>DialogFooter Variant
The DialogFooter component supports a variant prop to control its styling:
default(default): Includes a border-top, background color, and paddingbare: Removes the border and background for a minimal appearance
// Default variant (with border and background)
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<Button>Save</Button>
</DialogFooter>
// Bare variant (no border or background)
<DialogFooter variant="bare">
<DialogClose>Cancel</DialogClose>
<Button>Save</Button>
</DialogFooter>DialogPanel Scrolling
The DialogPanel component automatically wraps its content in a ScrollArea component. This means that if the content exceeds the dialog's maximum height, it will become scrollable automatically. The scrollbar will appear when needed, providing a smooth scrolling experience.
<DialogPanel>
{/* Long content that will scroll if it exceeds the dialog height */}
<div>...</div>
</DialogPanel>Examples
Open from a Menu
Demonstrates triggering a Dialog from a MenuItem, allowing dialogs to be opened as secondary actions from dropdowns.
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPopup,
DialogTitle,
} from "@/components/ui/dialog";
import {
Menu,
MenuItem,
MenuPopup,
MenuTrigger,
} from "@/components/ui/menu";
export default function Particle() {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<>
<Menu>
<MenuTrigger render={<Button variant="outline" />}>
Open menu
</MenuTrigger>
<MenuPopup align="start">
<MenuItem onClick={() => setDialogOpen(true)}>Open dialog</MenuItem>
</MenuPopup>
</Menu>
<Dialog onOpenChange={setDialogOpen} open={dialogOpen}>
<DialogPopup>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>Change your preferences</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Close</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
</>
);
}
Bare Footer
Uses DialogFooter variant="bare" to remove the border and background from the footer for a minimal, form-like appearance.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Open Dialog
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here. Click save when you're done.
</DialogDescription>
</DialogHeader>
<Form className="contents">
<DialogPanel className="grid gap-4">
<Field>
<FieldLabel>Name</FieldLabel>
<Input defaultValue="Margaret Welsh" type="text" />
</Field>
<Field>
<FieldLabel>Username</FieldLabel>
<Input defaultValue="@maggie.welsh" type="text" />
</Field>
</DialogPanel>
<DialogFooter variant="bare">
<DialogClose render={<Button variant="ghost" />}>
Cancel
</DialogClose>
<Button type="submit">Save</Button>
</DialogFooter>
</Form>
</DialogPopup>
</Dialog>
);
}
With Scroll
Tall content inside DialogPanel becomes automatically scrollable via the built-in ScrollArea wrapper.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export default function Particle() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Terms & Conditions
</DialogTrigger>
<DialogPopup className="sm:max-w-md" showCloseButton={false}>
<DialogHeader>
<DialogTitle>Terms & Conditions</DialogTitle>
</DialogHeader>
<DialogPanel>
<div className="flex flex-col gap-4 [&_strong]:font-semibold [&_strong]:text-foreground">
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<p>
<strong>Acceptance of Terms</strong>
</p>
<p>
By accessing and using this website, users agree to comply
with and be bound by these Terms of Service. Users who do not
agree with these terms should discontinue use of the website
immediately.
</p>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>User Account Responsibilities</strong>
</p>
<p>
Users are responsible for maintaining the confidentiality of
their account credentials. Any activities occurring under a
user's account are the sole responsibility of the account
holder. Users must notify the website administrators
immediately of any unauthorized account access.
</p>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>Content Usage and Restrictions</strong>
</p>
<p>
The website and its original content are protected by
intellectual property laws. Users may not reproduce,
distribute, modify, create derivative works, or commercially
exploit any content without explicit written permission from
the website owners.
</p>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>Limitation of Liability</strong>
</p>
<p>
The website provides content “as is” without any
warranties. The website owners shall not be liable for direct,
indirect, incidental, consequential, or punitive damages
arising from user interactions with the platform.
</p>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>User Conduct Guidelines</strong>
</p>
<ul className="list-disc pl-6">
<li>Not upload harmful or malicious content</li>
<li>Respect the rights of other users</li>
<li>
Avoid activities that could disrupt website functionality
</li>
<li>Comply with applicable local and international laws</li>
</ul>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>Modifications to Terms</strong>
</p>
<p>
The website reserves the right to modify these terms at any
time. Continued use of the website after changes constitutes
acceptance of the new terms.
</p>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>Termination Clause</strong>
</p>
<p>
The website may terminate or suspend user access without prior
notice for violations of these terms or for any other reason
deemed appropriate by the administration.
</p>
</div>
<div className="flex flex-col gap-1">
<p>
<strong>Governing Law</strong>
</p>
<p>
These terms are governed by the laws of the jurisdiction where
the website is primarily operated, without regard to conflict
of law principles.
</p>
</div>
</div>
</div>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
<Button type="button">I agree</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
Nested Dialogs
Opens a second dialog from within an already-open dialog, demonstrating stacked overlay behavior and layered focus trapping.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
export default function Particle() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Open parent
</DialogTrigger>
<DialogPopup showCloseButton={false}>
<DialogHeader>
<DialogTitle>Manage team member</DialogTitle>
<DialogDescription>
View and manage a user in your team.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-4">
<div className="grid gap-1">
<p className="text-muted-foreground text-sm">Name</p>
<p className="font-medium text-sm">Bora Baloglu</p>
</div>
<div className="grid gap-1">
<p className="text-muted-foreground text-sm">Email</p>
<p className="font-medium text-sm">bora@example.com</p>
</div>
</DialogPanel>
<DialogFooter>
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Edit details
</DialogTrigger>
<DialogPopup showCloseButton={false}>
<DialogHeader>
<DialogTitle>Edit details</DialogTitle>
<DialogDescription>
Make changes to the member's information.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-4">
<Field>
<FieldLabel>Name</FieldLabel>
<Input defaultValue="Bora Baloglu" type="text" />
</Field>
<Field>
<FieldLabel>Email</FieldLabel>
<Input defaultValue="bora@example.com" type="text" />
</Field>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>
Cancel
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
With Destructive Action
A confirmation dialog with a red destructive button for irreversible actions like permanently deleting content.
import { AlertTriangleIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Delete Item
</DialogTrigger>
<DialogContent>
<DialogHeader>
<div className="flex items-start gap-3">
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-destructive/10 text-destructive">
<AlertTriangleIcon className="size-5" />
</div>
<div className="flex flex-col gap-1">
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
item and remove all associated data from our servers.
</DialogDescription>
</div>
</div>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="outline" />}>
Cancel
</DialogClose>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
Cookie Preferences
A settings dialog with toggle switches for managing cookie consent categories such as analytics and marketing.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Cookie Preferences
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Cookie Preferences</DialogTitle>
<DialogDescription>
You can enable or disable different categories of cookies.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.75">
<Label className="font-medium text-sm">Essential Cookies</Label>
<p className="text-muted-foreground text-xs">
Required for the website to function properly. Cannot be
disabled.
</p>
</div>
<Switch defaultChecked disabled />
</div>
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.75">
<Label
className="font-medium text-sm"
htmlFor="cookie-analytics"
>
Analytics Cookies
</Label>
<p className="text-muted-foreground text-xs">
Help us understand how visitors interact with our website.
</p>
</div>
<Switch id="cookie-analytics" />
</div>
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.75">
<Label
className="font-medium text-sm"
htmlFor="cookie-marketing"
>
Marketing Cookies
</Label>
<p className="text-muted-foreground text-xs">
Used to deliver personalized advertisements and track ad
campaign performance.
</p>
</div>
<Switch id="cookie-marketing" />
</div>
</div>
<DialogFooter>
<Button variant="outline">Save Preferences</Button>
<Button>Accept All</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
Session Expired
A blocking dialog notifying the user their session has timed out, with a single "Sign in again" action button.
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function Pattern() {
return (
<div className="flex items-center justify-center">
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Session Expired
</DialogTrigger>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Session Expired</DialogTitle>
<DialogDescription>
Your session has timed out due to inactivity. Please sign in again
to continue where you left off.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button className="w-full">Sign In Again</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
Edit Profile
A dialog form for updating a user profile that includes an avatar upload area and editable input fields.
Margaret Welsh
margaret@example.com
import { EllipsisIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
export default function Component() {
return (
<div className="flex items-center gap-3">
<Avatar className="size-10">
<AvatarImage alt="Margaret Welsh" src="https://github.com/shadcn.png" />
<AvatarFallback>MW</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="font-medium text-sm leading-none">Margaret Welsh</p>
<p className="mt-0.5 truncate text-muted-foreground text-sm">
margaret@example.com
</p>
</div>
<Dialog>
<DialogTrigger
aria-label="Edit user details"
render={
<Button className="size-8 shrink-0" size="icon" variant="ghost" />
}
>
<EllipsisIcon className="size-4" />
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<div className="flex items-center gap-3">
<Avatar className="size-10">
<AvatarImage
alt="Margaret Welsh"
src="https://github.com/shadcn.png"
/>
<AvatarFallback>MW</AvatarFallback>
</Avatar>
<div>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Update your personal information.
</DialogDescription>
</div>
</div>
</DialogHeader>
<Form className="contents">
<DialogPanel className="grid gap-4">
<Field>
<FieldLabel>Name</FieldLabel>
<Input defaultValue="Margaret Welsh" type="text" />
</Field>
<Field>
<FieldLabel>Email</FieldLabel>
<Input defaultValue="margaret@example.com" type="email" />
</Field>
<Field>
<FieldLabel>Bio</FieldLabel>
<Textarea defaultValue="Product designer based in San Francisco." />
</Field>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>
Cancel
</DialogClose>
<Button type="submit">Save changes</Button>
</DialogFooter>
</Form>
</DialogPopup>
</Dialog>
</div>
);
}
Invite Team Members
A dialog for inviting users by email with a role selector and a scrollable list of pending invites.
import { UserPlusIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const members = [
{
avatar: "https://github.com/shadcn.png",
email: "margaret@example.com",
id: 1,
initials: "MW",
name: "Margaret Welsh",
role: "Owner",
},
{
avatar: "",
email: "bora@example.com",
id: 2,
initials: "BB",
name: "Bora Baloglu",
role: "Editor",
},
{
avatar: "",
email: "sofia@example.com",
id: 3,
initials: "SR",
name: "Sofia Reyes",
role: "Viewer",
},
];
export default function Component() {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
<UserPlusIcon className="size-4" />
Invite members
</DialogTrigger>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Invite to project</DialogTitle>
<DialogDescription>
Invite teammates by email and assign their access level.
</DialogDescription>
</DialogHeader>
<DialogPanel className="grid gap-5">
<div className="flex gap-2">
<Field className="min-w-0 flex-1">
<FieldLabel className="sr-only">Email address</FieldLabel>
<Input placeholder="colleague@example.com" type="email" />
</Field>
<Select defaultValue="viewer">
<SelectTrigger className="w-28 shrink-0">
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="viewer">Viewer</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectPopup>
</Select>
</div>
<div className="grid gap-1">
<p className="px-0.5 font-medium text-muted-foreground text-xs uppercase tracking-wide">
Members with access
</p>
<ul className="divide-y divide-border">
{members.map((member) => (
<li className="flex items-center gap-3 py-2.5" key={member.id}>
<Avatar className="size-8 shrink-0">
{member.avatar && (
<AvatarImage alt={member.name} src={member.avatar} />
)}
<AvatarFallback>{member.initials}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-sm leading-none">
{member.name}
</p>
<p className="mt-0.5 truncate text-muted-foreground text-xs">
{member.email}
</p>
</div>
<span className="shrink-0 text-muted-foreground text-xs">
{member.role}
</span>
</li>
))}
</ul>
</div>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
<Button>Send invite</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
Share Document
A share dialog with a copyable link field, a one-click copy button that switches to a checkmark on success, and permission-level selector tiles.
"use client";
import { CheckIcon, CopyIcon, LinkIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
const SHARE_LINK = "https://app.example.com/docs/q3-roadmap?share=abc123";
export default function Particle() {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(SHARE_LINK).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
<LinkIcon className="size-4" />
Share
</DialogTrigger>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Share document</DialogTitle>
<DialogDescription>
Anyone with the link can view this document.
</DialogDescription>
</DialogHeader>
<DialogPanel className="space-y-4">
<div className="flex gap-2">
<Input
className="flex-1 font-mono text-xs"
readOnly
value={SHARE_LINK}
/>
<Button
className="shrink-0"
onClick={handleCopy}
size="icon"
variant="outline"
>
{copied ? (
<CheckIcon className="size-4 text-green-500" />
) : (
<CopyIcon className="size-4" />
)}
</Button>
</div>
<div className="space-y-1">
<p className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
Link access
</p>
<div className="grid grid-cols-3 gap-2">
{["View only", "Can comment", "Can edit"].map((perm) => (
<button
className={`rounded-md border px-3 py-2 font-medium text-xs transition-colors ${
perm === "View only"
? "border-primary bg-primary/5 text-primary"
: "text-muted-foreground hover:border-foreground/30"
}`}
key={perm}
type="button"
>
{perm}
</button>
))}
</div>
</div>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Done</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
Two-Factor Authentication Setup
A 2FA setup dialog displaying a simulated QR code placeholder and a six-digit code entry — verifying the code locks the input and shows a success state.
//biome-ignore-all lint/suspicious/noArrayIndexKey:<>
"use client";
import { ShieldIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export default function Particle() {
const [code, setCode] = useState("");
const [verified, setVerified] = useState(false);
const handleVerify = () => {
if (code.length === 6) setVerified(true);
};
return (
<Dialog
onOpenChange={() => {
setCode("");
setVerified(false);
}}
>
<DialogTrigger render={<Button variant="outline" />}>
<ShieldIcon className="size-4" />
Enable 2FA
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Two-factor authentication</DialogTitle>
<DialogDescription>
Scan the QR code with your authenticator app, then enter the 6-digit
code to verify.
</DialogDescription>
</DialogHeader>
<DialogPanel className="space-y-5">
<div className="mx-auto flex size-40 items-center justify-center rounded-xl border bg-muted">
<div className="grid grid-cols-5 gap-1 p-3 opacity-40">
{Array.from({ length: 25 }).map((_, i) => (
<div
className={`size-4 rounded-sm ${Math.random() > 0.5 ? "bg-foreground" : ""}`}
key={i}
/>
))}
</div>
</div>
{verified ? (
<div className="rounded-md border border-green-200 bg-green-50 px-4 py-3 text-center dark:border-green-800 dark:bg-green-950">
<p className="font-medium text-green-700 text-sm dark:text-green-300">
2FA enabled successfully!
</p>
</div>
) : (
<div className="space-y-2">
<p className="text-center text-muted-foreground text-xs">
Enter the 6-digit code
</p>
<div className="flex justify-center gap-2">
{Array.from({ length: 6 }).map((_, i) => (
<div
className={`flex size-9 items-center justify-center rounded-md border text-center font-mono font-semibold text-sm ${
code[i]
? "border-primary bg-primary/5"
: "text-muted-foreground"
}`}
key={i}
>
{code[i] ?? "·"}
</div>
))}
</div>
<input
className="sr-only"
inputMode="numeric"
maxLength={6}
onChange={(e) =>
setCode(e.target.value.replace(/\D/g, "").slice(0, 6))
}
type="text"
value={code}
/>
</div>
)}
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Cancel</DialogClose>
<Button
disabled={code.length !== 6 || verified}
onClick={handleVerify}
>
{verified ? "Verified" : "Verify code"}
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
Subscription Cancellation
A cancellation dialog prompting the user to choose a reason before confirming — the destructive action stays disabled until a reason is selected.
"use client";
import { AlertTriangleIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
const REASONS = [
"Too expensive",
"Not using it enough",
"Missing features I need",
"Switching to a competitor",
"Other",
];
export default function Particle() {
const [reason, setReason] = useState("");
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
Cancel subscription
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<div className="flex size-10 items-center justify-center rounded-full bg-destructive/10">
<AlertTriangleIcon className="size-5 text-destructive" />
</div>
<DialogTitle>Cancel your plan</DialogTitle>
<DialogDescription>
We're sorry to see you go. Help us improve by telling us why
you're leaving.
</DialogDescription>
</DialogHeader>
<DialogPanel className="space-y-2">
{REASONS.map((r) => (
<button
className={`flex w-full items-center gap-3 rounded-md border px-3 py-2.5 text-left text-sm transition-colors ${
reason === r
? "border-destructive bg-destructive/5 text-destructive"
: "text-muted-foreground hover:border-foreground/20 hover:text-foreground"
}`}
key={r}
onClick={() => setReason(r)}
type="button"
>
<span
className={`size-4 shrink-0 rounded-full border-2 ${
reason === r
? "border-destructive bg-destructive"
: "border-muted-foreground"
}`}
/>
{r}
</button>
))}
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>
Keep plan
</DialogClose>
<Button disabled={!reason} variant="destructive">
Confirm cancellation
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
API Key Generation
Generates a new secret API key with mask/reveal toggle, one-click copy with confirmation feedback, and a regenerate button — all within a secure disclosure flow.
"use client";
import {
CopyIcon,
EyeIcon,
EyeOffIcon,
KeyIcon,
RefreshCwIcon,
} from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
function generateKey() {
return `sk_live_${Array.from({ length: 32 }, () =>
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".charAt(
Math.floor(Math.random() * 62),
),
).join("")}`;
}
export default function Particle() {
const [apiKey, setApiKey] = useState(generateKey);
const [revealed, setRevealed] = useState(false);
const [copied, setCopied] = useState(false);
const masked = apiKey.slice(0, 7) + "•".repeat(32) + apiKey.slice(-4);
const handleCopy = () => {
navigator.clipboard.writeText(apiKey).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Dialog
onOpenChange={() => {
setRevealed(false);
setCopied(false);
}}
>
<DialogTrigger render={<Button variant="outline" />}>
<KeyIcon className="size-4" />
Generate API key
</DialogTrigger>
<DialogPopup className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Your new API key</DialogTitle>
<DialogDescription>
Copy and store it now — you won't be able to see it again after
closing this dialog.
</DialogDescription>
</DialogHeader>
<DialogPanel className="space-y-4">
<div className="rounded-lg border bg-muted/40 p-4">
<p className="break-all font-mono text-muted-foreground text-xs">
{revealed ? apiKey : masked}
</p>
</div>
<div className="flex gap-2">
<Button className="flex-1" onClick={handleCopy} variant="outline">
<CopyIcon className="size-4" />
{copied ? "Copied!" : "Copy key"}
</Button>
<Button
onClick={() => setRevealed((v) => !v)}
size="icon"
variant="outline"
>
{revealed ? (
<EyeOffIcon className="size-4" />
) : (
<EyeIcon className="size-4" />
)}
</Button>
<Button
onClick={() => {
setApiKey(generateKey());
setRevealed(false);
setCopied(false);
}}
size="icon"
variant="outline"
>
<RefreshCwIcon className="size-4" />
</Button>
</div>
<p className="text-muted-foreground text-xs">
This key grants full API access. Never share it in public
repositories or client-side code.
</p>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button />}>Done</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
Product Feedback Rating
A star-rating feedback form for multiple product aspects (Ease of use, Performance, Design, Support) with an optional comment textarea — Submit stays disabled until every category is rated.
"use client";
import { StarIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogDescription,
DialogFooter,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
const ASPECTS = ["Ease of use", "Performance", "Design", "Support"];
export default function Particle() {
const [ratings, setRatings] = useState<Record<string, number>>({});
const [hover, setHover] = useState<Record<string, number>>({});
const [comment, setComment] = useState("");
const [submitted, setSubmitted] = useState(false);
const allRated = ASPECTS.every((a) => ratings[a]);
if (submitted) {
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
<StarIcon className="size-4" />
Leave feedback
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogPanel className="space-y-3 py-8 text-center">
<div className="mx-auto flex size-14 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/30">
<StarIcon className="size-7 fill-amber-400 text-amber-400" />
</div>
<DialogTitle>Thank you!</DialogTitle>
<DialogDescription>
Your feedback helps us improve the product for everyone.
</DialogDescription>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button className="w-full" />}>
Close
</DialogClose>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
<StarIcon className="size-4" />
Leave feedback
</DialogTrigger>
<DialogPopup className="sm:max-w-sm">
<DialogHeader>
<DialogTitle>Rate your experience</DialogTitle>
<DialogDescription>
How are we doing? Rate each aspect and leave a comment.
</DialogDescription>
</DialogHeader>
<DialogPanel className="space-y-4">
{ASPECTS.map((aspect) => (
<div className="flex items-center justify-between" key={aspect}>
<span className="text-sm">{aspect}</span>
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<button
className="p-0.5"
key={star}
onClick={() =>
setRatings((p) => ({ ...p, [aspect]: star }))
}
onMouseEnter={() =>
setHover((p) => ({ ...p, [aspect]: star }))
}
onMouseLeave={() =>
setHover((p) => ({ ...p, [aspect]: 0 }))
}
type="button"
>
<StarIcon
className={`size-5 transition-colors ${
star <= (hover[aspect] ?? ratings[aspect] ?? 0)
? "fill-amber-400 text-amber-400"
: "text-muted-foreground"
}`}
/>
</button>
))}
</div>
</div>
))}
<Textarea
onChange={(e) => setComment(e.target.value)}
placeholder="Any additional comments? (optional)"
rows={3}
value={comment}
/>
</DialogPanel>
<DialogFooter>
<DialogClose render={<Button variant="ghost" />}>Skip</DialogClose>
<Button disabled={!allRated} onClick={() => setSubmitted(true)}>
Submit feedback
</Button>
</DialogFooter>
</DialogPopup>
</Dialog>
);
}

