A modal window that interrupts the page for a focused task, form or confirmation. Traps focus, locks scroll, restores focus on close, and supports alert, full-screen and controlled modes.
role="alertdialog" is for confirmations that need an explicit decision. It can't be dismissed by clicking outside and hides the close icon. DialogIcon adds a tinted status icon.
import { TriangleAlertIcon } from "lucide-react";import { Button } from "@/components/ui/button";import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogIcon, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";export default function AlertDialogDemo() { return ( <DialogTrigger> <Button variant="outline" color="danger"> Delete project </Button> <DialogContent role="alertdialog" size="sm"> <div className="flex gap-4"> <DialogIcon tone="danger"> <TriangleAlertIcon /> </DialogIcon> <div className="grid gap-1.5"> <DialogTitle>Delete this project?</DialogTitle> <DialogDescription> All deployments and logs will be permanently removed. This can't be undone. </DialogDescription> </div> </div> <DialogFooter> <DialogClose>Cancel</DialogClose> <DialogClose variant="solid" color="danger"> Delete </DialogClose> </DialogFooter> </DialogContent> </DialogTrigger> );}
size="full" fills the viewport with a 1rem margin — useful for editors, previews and complex forms on small screens. Give the main element flex-1 to fill the space between header and footer.
import { Button } from "@/components/ui/button";import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";export default function DialogFullscreen() { return ( <DialogTrigger> <Button variant="outline">Open editor</Button> <DialogContent size="full"> <DialogHeader> <DialogTitle>Edit README.md</DialogTitle> <DialogDescription> Changes are saved when you press Save. </DialogDescription> </DialogHeader> <textarea aria-label="Content" defaultValue={ "# Desyne\n\nAccessible components for your shadcn project." } className="min-h-0 w-full flex-1 resize-none rounded-lg border bg-muted/40 p-4 font-mono text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/20" /> <DialogFooter> <DialogClose>Discard</DialogClose> <DialogClose variant="solid">Save</DialogClose> </DialogFooter> </DialogContent> </DialogTrigger> );}
Dialogs never grow taller than the viewport. Wrap long content in DialogBody and it scrolls while the header and footer stay put. Pass a max-h-* class to DialogContent for a shorter cap.
import { Button } from "@/components/ui/button";import { DialogBody, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";export default function DialogScrollable() { return ( <DialogTrigger> <Button variant="outline">Terms of service</Button> <DialogContent className="max-h-[80vh]"> <DialogHeader> <DialogTitle>Terms of service</DialogTitle> <DialogDescription>Last updated September 2026.</DialogDescription> </DialogHeader> <DialogBody className="space-y-3 text-muted-foreground leading-relaxed"> {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => ( <p key={n}> {n}. The content scrolls inside the dialog while the header and footer stay put. Long legal text, changelogs and review screens work well this way. </p> ))} </DialogBody> <DialogFooter> <DialogClose>Decline</DialogClose> <DialogClose variant="solid">Accept</DialogClose> </DialogFooter> </DialogContent> </DialogTrigger> );}
Render DialogContent without a trigger and control it with isOpen and onOpenChange. Use this to open a dialog from a menu item, a keyboard shortcut or after an async event. Focus returns to the element that was focused before opening.
import { MoreHorizontalIcon, PencilIcon } from "lucide-react";import { useState } from "react";import { Button } from "@/components/ui/button";import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle,} from "@/components/ui/dialog";import { MenuContent, MenuItem, MenuTrigger } from "@/components/ui/menu";import { TextField } from "@/components/ui/text-field";/** Open a dialog from a menu item: control it with `isOpen` instead of a trigger. */export default function DialogControlled() { const [isOpen, setOpen] = useState(false); return ( <> <MenuTrigger> <Button variant="outline" size="icon" aria-label="Actions"> <MoreHorizontalIcon /> </Button> <MenuContent> <MenuItem textValue="Rename" onAction={() => setOpen(true)}> <PencilIcon /> Rename… </MenuItem> </MenuContent> </MenuTrigger> <DialogContent isOpen={isOpen} onOpenChange={setOpen} size="sm"> <DialogHeader> <DialogTitle>Rename file</DialogTitle> <DialogDescription> Enter a new name for “roadmap.pdf”. </DialogDescription> </DialogHeader> <TextField aria-label="File name" defaultValue="roadmap.pdf" autoFocus /> <DialogFooter> <DialogClose>Cancel</DialogClose> <Button onPress={() => setOpen(false)}>Rename</Button> </DialogFooter> </DialogContent> </> );}
isDismissable={false} ignores outside clicks, isKeyboardDismissDisabled ignores Esc, and showCloseButton={false} hides the ✕. Use sparingly — users should always have a visible way out.
import { Button } from "@/components/ui/button";import { DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger,} from "@/components/ui/dialog";export default function DialogNonDismissable() { return ( <DialogTrigger> <Button variant="outline">Accept terms</Button> <DialogContent isDismissable={false} isKeyboardDismissDisabled showCloseButton={false} > <DialogHeader> <DialogTitle>Updated terms of service</DialogTitle> <DialogDescription> Clicking outside or pressing Esc won’t close this dialog. Choose an option to continue. </DialogDescription> </DialogHeader> <DialogFooter> <DialogClose>Decline</DialogClose> <DialogClose variant="solid">Accept</DialogClose> </DialogFooter> </DialogContent> </DialogTrigger> );}
Focus moves into the dialog on open (to the first focusable element, or one with autoFocus), stays trapped inside, and returns to the trigger on close.
Page scroll is locked while open, and content outside the dialog is hidden from screen readers with aria-hidden.
DialogTitle provides the accessible name. Without a visible title, pass aria-label to DialogContent.
role="alertdialog" tells screen readers the dialog needs an immediate response.
Put autoFocus on the least destructive action in alert dialogs, or on the first field in forms.
Nested overlays (selects, menus, popovers) inside a dialog work as expected; Esc closes the innermost one first.
Enter and exit animations use tw-animate-css classes keyed off data-entering and data-exiting, so React Aria waits for them to finish before unmounting.
className on DialogContent styles the panel (width, height, padding, radius).
The backdrop uses the exported overlayStyles string; reuse it for your own modal surfaces.
The panel is a flex column capped at the viewport height; the inner Dialog is flex flex-col gap-4 p-6 and scrolls as a fallback if you don't use DialogBody.
The inner Dialog padding is p-6. Override with [&>[role=dialog]]:p-0 on DialogContent for edge-to-edge content like images.