Dialog
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
Installation
npx shadcn@latest add @redpanda/dialogpnpm dlx shadcn@latest add @redpanda/dialogyarn dlx shadcn@latest add @redpanda/dialogbunx shadcn@latest add @redpanda/dialognubx shadcn@latest add @redpanda/dialogaube dlx shadcn@latest add @redpanda/dialogWhen to use
Use this decision tree to determine when to use the Dialog component:
Usage
<Dialog> <DialogTrigger>Open Dialog</DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>Title</DialogTitle> <DialogDescription>Description</DialogDescription> </DialogHeader> <p>Dialog Content</p> <DialogFooter> <button>Close</button> </DialogFooter> </DialogContent></Dialog>Props
Examples
Default
Delete
No Header
Skip the header when the dialog body stands on its own — the close button stays at the top-right and the top border is omitted since there’s nothing to separate from.
No Footer
Omit the footer when the dialog is informational and doesn’t require explicit actions beyond dismissing.
Scrolling content
DialogContent caps at max-h-[85vh] and DialogBody is the scrollable region — long content gets a pinned header/footer with an internal scrollbar for free. When the body overflows, fading top/bottom shadows appear automatically to signal “more above/below”; opt out with <DialogBody scrollShadow={false}>.
Fixed height
Pass height="sm" | "md" | "lg" | "xl" on DialogContent to lock the dialog to a stable pixel height (clamped to 85vh). This is useful for multi-view dialogs — wizards, step flows, gallery → detail — where the container shouldn’t reflow as the user navigates between views.
Animated auto height
In the default height="auto" mode, the dialog smoothly animates between natural heights when its content changes — toggling sections, async-loaded data, expanding details. This height transition is powered by Motion. No prop opt-in needed; it’s automatic for auto-height dialogs and respects prefers-reduced-motion.
No Header or Footer
Use body-only dialogs sparingly — without a visible header the dialog still needs an accessible name, so render a DialogTitle with className="sr-only" for screen readers. The dialog owns close-button clearance for a direct DialogBody when no DialogHeader is present. Do not add body-padding overrides: the standard headerless inset keeps text clear of the close action. Headered dialogs and dialogs with showCloseButton={false} retain their existing body padding.
Footer with helper text
When the footer pairs metadata or status text (e.g. “Changes are saved automatically”) with a single dismiss action, use justify="between" so the text sits on the left and the button on the right. The footer centers items vertically by default, so the text baseline aligns cleanly with the button.
Component Props
DialogContent
size?'sm' | 'md' | 'lg' | 'xl' | 'full'
Maximum width breakpoint of the dialog.
'sm' | 'md' | 'lg' | 'xl' | 'full''md'variant?'standard' | 'centered' | 'destructive'
Visual treatment of the dialog frame.
'standard' | 'centered' | 'destructive''standard'height?'auto' | 'sm' | 'md' | 'lg' | 'xl'
Vertical sizing. `auto` grows with content (capped at 85vh) and animates between natural heights when content changes. The fixed sizes lock the dialog to a stable pixel height (each clamped to 85vh) so multi-view dialogs don't reflow between steps.
'auto' | 'sm' | 'md' | 'lg' | 'xl''auto'showCloseButton?boolean
Render the top-right close affordance.
booleantrueshowOverlay?boolean
Render the dimmed backdrop behind the dialog.
booleantrueDialogBody
padding?'none' | 'sm' | 'md' | 'lg'
Padding around the body content. The inner content is wrapped so scroll shadows can sit flush against the body edges — set `padding` here rather than overriding via `className`, which targets the outer scroll container.
'none' | 'sm' | 'md' | 'lg''md'spacing?'none' | 'sm' | 'md' | 'lg'
Vertical gap between direct children of the body.
'none' | 'sm' | 'md' | 'lg''md'scrollShadow?boolean
Show fading top/bottom shadows when the body overflows, cueing the user that there is more content above or below.
booleantrueKeyboard interactions
| Key | Action |
|---|---|
Enter / Space on DialogTrigger |
Opens the dialog and moves focus to the first focusable element inside DialogContent. |
Tab / Shift+Tab |
Cycles focus within the dialog — focus is trapped while open. |
Escape |
Closes the dialog and restores focus to the trigger. |
Click outside DialogContent |
Closes the dialog (modal scrim dismiss). |
Accessibility
- Render a
DialogTitle(or a visually-hidden one) — Base UI enforces this with a development warning. The dialog is labelled viaaria-labelledbypointing at the title. - Add a
DialogDescriptionwhen the dialog’s purpose isn’t obvious from the title; it is wired up viaaria-describedby. - Focus is trapped inside the popup while open and returned to the trigger on close. Background content is marked
aria-hiddenand made inert so assistive tech doesn’t reach it. onOpenChangeuses Base UI’s(open: boolean, eventDetails) => voidsignature. The second argument carries the close reason (e.g.'escape-key','outside-press','close-press') and is optional to consume — existing Radix-style single-argument handlers keep working unchanged.
Controlled vs uncontrolled
Uncontrolled — let the dialog own open state, react via onOpenChange:
<Dialog onOpenChange={(open) => console.log('open:', open)}> <DialogTrigger>Open</DialogTrigger> <DialogContent>{/* ... */}</DialogContent></Dialog>Controlled — drive open from parent state, e.g. to open the dialog from a menu item or remote action:
const [open, setOpen] = useState(false);
<Dialog open={open} onOpenChange={setOpen}> <DialogTrigger>Open</DialogTrigger> <DialogContent> <DialogFooter> <Button onClick={() => setOpen(false)}>Done</Button> </DialogFooter> </DialogContent></Dialog>;