Accessible modal dialogs with focus management.
Dialog
The Dialog component is a modal overlay built on Radix UI's dialog primitive. It handles focus trapping, escape-to-close, click-outside-to-close, and scroll locking — all the tricky stuff done right.
Import#
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@aidimension/ui/components/dialog";Basic usage#
<Dialog>
<DialogTrigger asChild>
<Button>Open dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>Controlled#
const [open, setOpen] = useState(false);
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
{/* ... */}
<Button onClick={() => setOpen(false)}>Close</Button>
</DialogContent>
</Dialog>With form#
<Dialog>
<DialogTrigger asChild>
<Button>Add user</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a new user</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="space-y-3">
<div>
<Label htmlFor="name">Name</Label>
<Input id="name" />
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />
</div>
</div>
<DialogFooter className="mt-6">
<Button type="submit">Invite</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>Sizes#
Use the className prop to control width:
<DialogContent className="sm:max-w-sm">Small</DialogContent>
<DialogContent className="sm:max-w-md">Default</DialogContent>
<DialogContent className="sm:max-w-lg">Large</DialogContent>
<DialogContent className="sm:max-w-2xl">XL</DialogContent>
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">Full</DialogContent>API reference#
| Component | Description |
|---|---|
Dialog | The root. Manages open state and provides context. |
DialogTrigger | The element that opens the dialog. |
DialogContent | The modal panel. Renders into a portal. |
DialogHeader | Top section, contains title + description. |
DialogTitle | Required for screen readers. |
DialogDescription | Optional subtitle. |
DialogFooter | Bottom section, usually with action buttons. |
DialogClose | Closes the dialog when wrapped. |
Accessibility#
Built on Radix, so you get:
- ✅ Focus trapped inside the dialog
- ✅
Tabcycles through focusable elements - ✅
Escapecloses the dialog - ✅ Click outside closes (configurable)
- ✅
aria-modal,aria-labelledby,aria-describedbyset automatically - ✅ Body scroll locked while open
- ✅ Return focus to the trigger on close
Common patterns#
Confirmation dialog#
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete your account?</DialogTitle>
<DialogDescription>
This will permanently delete your account and all data. This cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button variant="destructive" onClick={handleDelete}>Yes, delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>Scrollable long content#
<DialogContent className="max-h-[90vh] overflow-y-auto">
{/* long content here */}
</DialogContent>Non-modal dialog#
Set modal={false} on the root. The dialog won't trap focus or lock scroll, and you can interact with content behind it.