Dialog
An overlay that interrupts the workflow to ask for a decision.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/dialog.jsonUsage
import {
Dialog, DialogClose, DialogContent, DialogDescription,
DialogFooter, DialogHeader, DialogTitle, DialogTrigger,
} from '@/components/ui/dialog'<Dialog>
<DialogTrigger render={<Button variant="secondary">Sign report</Button>} />
<DialogContent>
<DialogHeader>
<DialogTitle>Sign this report?</DialogTitle>
<DialogDescription>Signing locks the report from further edits.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="tertiary">Cancel</Button>} />
<DialogClose render={<Button>Sign and send</Button>} />
</DialogFooter>
</DialogContent>
</Dialog>A dialog stops the work in front of it. That is its whole value, and the reason to spend it
carefully: a reader who is dismissing dialogs on reflex is no longer reading them. Reach for one
when a decision genuinely cannot wait — signing, cancelling an order, discarding an unsaved
draft. Anything that only adds information alongside the page wants a Popover; anything the
reader can act on later wants an inline panel or an Alert.
Composition
<Dialog> {/* owns the open state */}
<DialogTrigger> {/* renders your button, via render= */}
<DialogContent> {/* portal + backdrop + popup, and the ✕ */}
<DialogHeader> {/* title and description, grouped for spacing */}
<DialogTitle> {/* becomes aria-labelledby */}
<DialogDescription> {/* becomes aria-describedby */}
</DialogHeader>
… {/* the body: yours */}
<DialogFooter> {/* right-aligned actions */}
<DialogClose> {/* dismisses without an onClick */}
</DialogFooter>
</DialogContent>
</Dialog>render replaces the primitive's element rather than wrapping it, so the trigger is your
button — one element, one set of handlers, no nested interactive nodes. The same applies to
DialogClose, which is how the footer buttons dismiss with no state of their own.
DialogContent bundles the portal, the backdrop and the popup. Everything you pass lands inside
the popup, so the ✕ in the corner sits above your content regardless of what that content is.
What you get for free
Focus moves into the dialog on open and returns to the trigger on close. Focus is trapped while
it is open. Escape dismisses. The rest of the page is inert to a screen reader. Scroll is locked.
aria-labelledby and aria-describedby are wired from DialogTitle and DialogDescription.
That list is the reason not to hand-roll a modal.
Always include a DialogTitle. Without it the dialog has no accessible name, and a screen
reader announces an unlabelled group. Use showCloseButton={false} if the corner ✕ is wrong
for your layout — never drop the title instead.
Widths
The stylesheet sets max-width: 460px, which fits a question and two buttons. Change it with a
max-w-* on DialogContent rather than a new component: a dialog holding a table needs the room,
and a dialog asking one question is harder to read at that width, not easier. The popup is always
calc(100vw - 32px) at most, so none of these overflow a phone.
A modal with a rail
Figma's "Modal — Navigation" (node 3336:6038) is 840×560 with a 240px rail beside a panel. Like
the other three modal frames it is a composition, not a component: a
NavList, a Header Bar, a body and an
Action Bar, inside a DialogContent with p-0.
Two things are easy to get wrong here.
The panel title has to be the DialogTitle. A dialog takes its accessible name from that
element, and a rail modal has an obvious-looking heading that is not it — so the dialog opens
announced as "dialog" and nothing else. Render one through the other rather than having both:
<HeaderBarTitle render={<DialogTitle />}>{section.label}</HeaderBarTitle>Scroll the rail and the body separately. One scroll container around the pair means a long
list of sections pushes the actions off the bottom, and the way back to Save is to scroll a list
you were not reading. Give each overflow-y-auto and leave the popup itself fixed.
Note also showCloseButton={false} — the header bar carries its own close, and the default one
would land on top of it.
A form inside a dialog
Two details make this work rather than merely render. The <form> carries an id and the submit
button in the footer carries form="…", so the button can live outside the form element and
still submit it — and Enter in any text field submits, which is what people expect of a form.
And the dialog is controlled, so it closes on a successful submit rather than on the click: a
DialogClose here would dismiss the form before validation had a say.
Give the first field no autoFocus. Base UI already moves focus to the first tabbable element,
and a second focus call fights it.
Long content
The popup is capped at calc(100vh - 64px) and scrolls as a whole. That is fine for a wall of
consent text, and wrong for anything with a footer — the buttons scroll away with the content.
Put a ScrollArea around the body instead, and the header and the actions stay where the reader
left them.
Controlled
Pass open and onOpenChange when something other than the trigger has to open or close it: a
keyboard shortcut, a route, or — as here — an action that must finish before the dialog goes
away. Note the confirm button is a plain Button rather than a DialogClose; Close dismisses
on click, which would hide the pending state and the failure with it.
Confirming something destructive
Use Alert Dialog rather than this component when the answer
matters and the wrong answer cannot be undone. It reports itself as role="alertdialog", it
focuses the safe option rather than the first one, and its buttons are named for what they do.
Escape still dismisses it — that is the platform behaviour and we have not overridden it, so
"are you sure" is a prompt, not a lock.
Keyboard
Tab cycles within the dialog and never escapes it. Escape closes. Shift+Tab from the first
element wraps to the last. On close, focus returns to the element that opened it — which is why
the trigger should be a real button and not a div with a handler.
API Reference
Everything Base UI's Dialog accepts. What we add
on DialogContent:
Prop
Type
On Dialog itself:
Prop
Type
The layout parts — DialogHeader, DialogFooter — are plain divs and take any div prop.