| name | modals-and-dialogs |
| description | Use when building modals, sheets, drawers, or confirmation flows — choosing Dialog vs Sheet vs Drawer vs AlertDialog, focus trapping, destructive-action confirms, and forms inside overlays without losing data. |
Modals & Dialogs
Overview
Overlays interrupt — use them sparingly and correctly. shadcn/ui (on Radix) gives you four primitives with built-in focus trap, Esc to close, scroll lock, and aria-modal. Your job is choosing the right one and handling the edge cases: destructive confirms, forms with unsaved input, and not stacking overlays.
When to use
- A focused task or confirmation needs to block the page.
- Building a side panel for details/filters/quick-edit.
- A
<div> overlay is being hand-rolled (you'll miss focus trap + keyboard).
Pick the right primitive
| Use | Primitive | When |
|---|
| Centered modal | Dialog | Short forms, focused tasks |
| Side panel | Sheet | Details, filters, edit alongside context |
| Bottom drawer | Drawer (vaul) | Mobile-friendly, touch dismiss |
| Confirm/destruct | AlertDialog | Yes/no decisions; no dismiss-on-outside-click |
Destructive confirmation
AlertDialog (not Dialog) for irreversible actions — it requires an explicit choice and won't close on outside click or Esc accidentally losing the decision:
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes the project and all its data. This can’t be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={deleteProject}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
For high-risk deletes, require typing the resource name to enable the button.
Forms inside dialogs
Control open so you can keep the dialog open on validation error and close only on success:
const [open, setOpen] = React.useState(false);
async function onSubmit(values) {
await save(values);
setOpen(false);
toast.success("Saved");
}
<Dialog open={open} onOpenChange={setOpen}>…</Dialog>
Warn before discarding unsaved changes: intercept onOpenChange and confirm if the form isDirty.
Accessibility (mostly handled, don't break it)
- Always include
DialogTitle (+ DialogDescription) — they back aria-labelledby/describedby. Visually hide the title with sr-only if needed, but keep it.
- Focus moves into the dialog on open and returns to the trigger on close — don't disable this.
- Don't stack modals. Replace content or use a multi-step flow instead.
Pitfalls
Dialog for destructive actions — outside-click dismiss can skip the decision; use AlertDialog.
- Closing on submit before the request resolves — close on success only.
- No unsaved-changes guard — users lose form input on accidental dismiss.
- Missing
DialogTitle — breaks the accessible name; Radix will warn.
- Modal-on-modal stacking — confusing focus; redesign the flow.
- Putting huge content in a modal — use a
Sheet or a full page instead.
Hand-off
Overlays that host forms-and-validation, confirm data-tables row actions, and pair with notifications-and-toasts for post-action feedback.