Dialog

A modal dialog for confirmations, forms, and focused tasks. Includes backdrop, keyboard dismissal, and focus trapping.

Basic usage

Compose with DialogTrigger, DialogContent, DialogHeader, and DialogFooter. Pass asChild to the trigger so it renders a real Button.

Controlled

Pass open and onOpenChange for full control — the way a dialog opens from a menu item, a table row or a keyboard shortcut. The button below only sets state; it is not a DialogTrigger, and closing still puts focus back on it: since 3.3.0, DialogContent returns focus to whatever had it when the dialog opened.

Sizes

size is a Comitor addition to the shadcn dialog. It maps to a static sm:max-w-* class so Tailwind can see it at build time; on narrow viewports every size falls back to max-w-[calc(100%-2rem)].

With form

DialogBody is the second Comitor addition: the region between header and footer that owns the scroll, so a long form never pushes the title or the actions out of view. Give it max-h-* only when you want a cap tighter than the dialog's own — the scrolling itself is already built in.

Close button

The corner ✕ is on by default. Turn it off with showCloseButton={false} when the footer should be the only exit, and keep at least one DialogClose in reach. When you keep it, its screen-reader name is closeLabel: “Close” by default, “Đóng” under a Vietnamese ComitorLocaleProvider, and your own string when you pass one — worth doing when “Close” alone would not say which panel goes.

Dialog Props

PropTypeDefaultDescription
openboolean—Controlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseOpen state on first render, for uncontrolled usage.
onOpenChange(open: boolean) => void—Called whenever the dialog opens or closes.
modalbooleantrueWhen true, content outside the dialog is inert and hidden from assistive technology.
childrenrequiredReact.ReactNode—Dialog trigger and content.

DialogContent Props

PropTypeDefaultDescription
size'sm' | 'md' | 'lg' | 'xl''md'Max width from the sm breakpoint up (sm:max-w-sm … sm:max-w-xl). Below sm the panel is always max-w-[calc(100%-2rem)]. Package addition — not in the shadcn baseline.
showCloseButtonbooleantrueRender the ✕ button in the top-right corner of the panel.
closeLabelstring'Close'Visually hidden label on that close button. Follows ComitorLocaleProvider — “Đóng” under locale="vi" — and a value passed here wins, e.g. 'Close invoice preview' when “Close” alone would not say which panel goes.
classNamestring—Merged onto the panel with cn(); use it for one-off width or padding overrides.
onCloseAutoFocus(event: Event) => void—Runs as the dialog closes, before focus moves — ahead of both Radix and the package's own focus return, so it wins: call event.preventDefault() and focus what should come next. Keep one that sends focus somewhere else, such as the next row after a delete; one that only re-focused the opener can go, since DialogContent does that itself (see Accessibility).
…Radix ContentDialogPrimitive.ContentProps—Everything else is forwarded to Radix Dialog.Content — onEscapeKeyDown, onInteractOutside, onOpenAutoFocus, forceMount.

Parts

Every part accepts className, which is merged onto the defaults with cn().

PropTypeDefaultDescription
DialogDialogPrimitive.Root—Stateful container. Holds the open state and the trigger/content pair.
DialogTriggerDialogPrimitive.Trigger—Control that opens the dialog. Use asChild to render your own Button.
DialogPortalDialogPrimitive.Portal—Portal target. DialogContent already renders one — reach for it only when building a custom content shell.
DialogOverlayDialogPrimitive.Overlay—Dimmed backdrop (bg-ink/60, dark:bg-ink/80, backdrop-blur). Also rendered for you by DialogContent.
DialogContentDialogPrimitive.Content—The centred panel: portal + overlay + card surface + optional close button.
DialogHeaderdiv—flex flex-col gap-2, centred on mobile and left-aligned from sm up.
DialogBodydiv—Scrolling content region between header and footer — the package applies -m-1 min-h-0 flex-auto overflow-y-auto p-1, so the header and footer stay pinned and the focus ring on a w-full child is not clipped. Package addition.
DialogFooterdiv—Actions row: stacked and reversed on mobile, right-aligned from sm up.
DialogTitleDialogPrimitive.Title—Accessible name for the panel. Always include one.
DialogDescriptionDialogPrimitive.Description—Supporting text wired to aria-describedby.
DialogCloseDialogPrimitive.Close—Dismisses the dialog. Use asChild to wrap your own Button.

Accessibility

  • Built on Radix Dialog: the panel carries role="dialog" and aria-modal="true", and focus is trapped while it is open.
  • On close, focus returns to the DialogTrigger — or, with no Trigger, to the element that had focus just before the dialog opened: the button that set open, a table row, an ActionBar action. Radix only covers the Trigger; the rest is the package’s, since 3.3.0 — before it, a dialog opened from state dropped focus on <body> (WCAG 2.4.3). Since 3.3.1 a field with autoFocus inside the dialog no longer defeats it.
  • Opened from a DropdownMenu item, which unmounts with the menu, focus goes to the menu’s trigger button — out of submenus, to the root one. The package acts only when focus has really been lost, and it never guesses: when the opener is gone, say the row the dialog just deleted, nothing is focused. That is the job for onCloseAutoFocus, which runs first and wins once it calls event.preventDefault().
  • Not covered: content kept mounted with forceMount, which records the opener only when Radix runs its open auto-focus, and Drawer, which is vaul’s. Return focus yourself in onCloseAutoFocus there.
  • Always include a DialogTitle — it supplies aria-labelledby; DialogDescription supplies aria-describedby.
  • Escape and a click on the backdrop both close the dialog. Intercept them with onEscapeKeyDown / onInteractOutside when there is unsaved work — or reach for AlertDialog, which refuses outside clicks by design.
  • Body scroll is locked and the rest of the page is inert while modal is true.
  • The corner close button has no visible text — its accessible name comes from the visually hidden closeLabel, which is “Close” by default and follows the language of the nearest ComitorLocaleProvider. Pass your own when the page needs something more specific.