Dialog
A modal dialog that interrupts the user with important content. Built on Radix UI with focus trap, escape handling, and scroll lock.
Form
Share link
Installation
pnpm dlx @hex-core/cli add dialogControlled with onOpenChange guard
Block accidental close while the form is dirty. onOpenChange swallows the request to close and prompts the user instead
const [open, setOpen] = useState(false);
const [isDirty, setIsDirty] = useState(false);
function handleOpenChange(next: boolean) {
if (!next && isDirty) {
const confirmed = window.confirm("Discard your changes?");
if (!confirmed) return;
setIsDirty(false);
}
setOpen(next);
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>Update your name and bio.</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input
placeholder="Name"
onChange={() => setIsDirty(true)}
/>
</div>
<DialogFooter>
<Button onClick={() => { setIsDirty(false); setOpen(false); }}>Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);Long content with scroll
Pin DialogHeader and DialogFooter outside a scrolling region so the title and primary actions stay visible while the body scrolls
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Read terms</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Terms of service</DialogTitle>
<DialogDescription>Last updated April 2026.</DialogDescription>
</DialogHeader>
<div className="max-h-[60vh] overflow-y-auto pr-2 text-sm leading-relaxed">
{sections.map((section) => (
<section key={section.id} className="py-3">
<h3 className="font-medium">{section.title}</h3>
<p className="mt-1 text-muted-foreground">{section.body}</p>
</section>
))}
</div>
<DialogFooter>
<Button variant="outline">Decline</Button>
<Button>Accept</Button>
</DialogFooter>
</DialogContent>
</Dialog>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Default open state for uncontrolled usage |
onOpenChange | function | — | Callback fired when open state changes: (open: boolean) => void |
modal | boolean | true | When true, content outside the dialog is inert |
AI Guidance
When to use
Use for focused, interruptive tasks: confirmations, quick forms, detail views. The user must address the dialog before continuing.
When not to use
Don't use for destructive confirmations (use AlertDialog). Don't use for complex multi-step flows (use a full page). Don't use for non-critical info (use Tooltip or Popover).
Common mistakes
- Nesting dialogs inside each other
- Forgetting DialogTitle (breaks accessibility — screen readers need it)
- Using DialogDescription for long-form content (keep it short)
- Putting too many primary actions in DialogFooter
Accessibility
Radix traps focus, handles Escape to close, and wires aria-labelledby/describedby to DialogTitle/DialogDescription. Always include a DialogTitle. DialogContent is constrained to `max-h-[calc(100vh-2rem)]` and scrolls internally so long content stays inside the focus trap.
Related components
Token budget: 600
Verified against @hex-core/components@1.12.0