Dialog
A modal overlay — a backdrop, a centred popup, an optional corner close button and a plain, right-aligned footer that stays put while the body scrolls.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (modal)
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Dialog before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify dialogInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/dialogRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/use-modal-inert.
Usage
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
<Dialog>
<DialogTrigger>Open</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your account
and remove your data from our servers.
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>;Anatomy
Examples
Composition
DialogContent renders the portal and the backdrop for you, so the tree below is the whole
component: a trigger beside a content, and inside it a header and a footer.
Dialog
├── DialogTrigger
└── DialogContent
├── DialogHeader
│ ├── DialogTitle
│ └── DialogDescription
└── DialogFooterCustom Close Button
DialogClose closes the dialog from anywhere inside it. Give it a render prop and it becomes
that control — here a footer button — while keeping the close behaviour.
No Close Button
showCloseButton={false} on DialogContent removes the corner close control. DialogFooter has
its own showCloseButton, off by default, which appends a secondary Close button to the footer —
so a dialog without the corner control still offers a visible way out.
Sticky Footer
DialogFooter is as plain as the header — no background, no divider, the popup's own padding —
with its actions right-aligned and secondary actions (Cancel, Back) on variant="secondary"
(OVL-18). Put the scroll container between the header and
the footer and the actions stay visible while the body scrolls.
Scrollable Content
The popup itself does not scroll — it is a grid that grows with its children. Long content goes in
its own overflow-y-auto element so the header stays in view.
Size
size on DialogContent steps the width cap from the sm breakpoint up. The value is published
as data-size, the same axis AlertDialogContent carries. Below sm every size fills the viewport
less a 1rem gutter.
| Size | Cap | Use it for |
|---|---|---|
sm | max-w-xs (320px) | A one-field prompt |
default | max-w-sm (384px) | A short form or a confirmation with a little context |
md | max-w-lg (512px) | Share and settings dialogs: people rows with a level menu, tabs |
lg | max-w-2xl (672px) | A two-column form, a long create flow |
xl | max-w-5xl (1024px) | A bulk-edit table or an attribute editor |
fullscreen | The viewport less a 16px inset (none on a phone) | A full-screen surface — a document's version history beside its text |
fullscreen is also a height: the popup is h-[calc(100dvh-2rem)], a flex column whose header and
footer stay put while a DialogBody (or a VersionCompare) between them scrolls. Below sm it is
the whole screen, with no border or radius, padded clear of the safe areas.
RTL
The popup's position, its close button and its inner padding are written in logical properties, so
a right-to-left document mirrors the whole surface; the -translate-x-1/2 that centres it is not
logical, so it carries an explicit rtl: counterpart. Two things the caller owns: wrap the subtree
in DirectionProvider so Base UI's own logic agrees with the document's dir, and hand dir to
DialogContent — the popup portals to <body>, so a dir on an ancestor of the trigger never
reaches it.
Scrolling body
DialogBody is the scroller for long content (min-h-0 flex-1 overflow-y-auto, with -mx-4 px-4
so focus outlines are not clipped at the popup's padding). A popup that holds one becomes a flex
column capped 1rem inside the viewport, so the header and the footer stay in view while the
body scrolls. A dialog without a DialogBody keeps upstream's growing grid.
Long names
A dialog that carries a user-entered name — a product, a task, a file — keeps every part inside the
popup. DialogContent lays out on one minmax(0, 1fr) column, so a control that does not wrap (a
select showing a long value) cannot widen the popup past its cap, and the popup breaks a long word
with no spaces rather than letting it run past the edge. A title that wraps sits on snug lines, and
while the corner close × shows, DialogHeader keeps clear of it, so the title wraps before the ×. To
show a picked name whole instead of cutting it off, give the picker wrap
(Searchable Select, Select).
A flex row that must shrink below its longest word — a title beside a hint icon — still needs
min-w-0 on the shrinking child.
Playground
Retune the content's max-width cap and either close button, then copy the generated JSX. The dialog renders closed; open it from the trigger.
<Dialog>
<DialogTrigger render={<Button variant="outline">Open dialog</Button>} />
<DialogContent>
<DialogHeader>
<DialogTitle>Delete project</DialogTitle>
<DialogDescription>This action cannot be undone.</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose render={<Button variant="secondary">Cancel</Button>} />
<Button variant="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>API Reference
Dialog adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
| Prop | Type | Default | Description |
|---|---|---|---|
closeLabel | string | — | |
showCloseButton | boolean | — | |
size | "default" | "fullscreen" | "lg" | "md" | "sm" | "xl" | — |
| Prop | Type | Default | Description |
|---|---|---|---|
closeLabel | string | — | |
showCloseButton | boolean | — |
closeLabel (default "Close") names the built-in close control on both DialogContent (the
corner button) and DialogFooter (its opt-in secondary button).
DialogTrigger, DialogClose, DialogPortal, DialogOverlay, DialogHeader, DialogBody,
DialogTitle and DialogDescription add no props of their own — each accepts everything its Base UI part or its
div accepts.
Width is the size prop on DialogContent (OVL-16). At size="default" the cap is a plain
sm:max-w-sm, so a one-off className="sm:max-w-md" still overrides it; prefer a size step.
Accessibility
- Renders Base UI's Dialog: the popup is
role="dialog"witharia-modal, wired toDialogTitlethrougharia-labelledbyand toDialogDescriptionthrougharia-describedby. Esc closes, and focus is trapped inside the popup and returned to the trigger. - Give every dialog a
DialogTitle. Without one the popup has no accessible name; if the title must not be seen, keep it and addclassName="sr-only". - The background is made inert natively, not just focus-trapped (A11Y-9), so a focus guard that
hands focus to
<body>cannot tab back into the page behind. Withmodal={false}the background stays interactive and nothing is made inert, which is the point of that mode. - The popup portals to
<body>, soDialogPortalre-applies the enclosing theme scope on adisplay: contentshost inside the portal (OVL-13) — a scoped or dark surface survives the jump. - The corner close button is an icon-only
Buttoncarrying asr-onlylabel, "Close" unlesscloseLabelnames it.
| Contract | States tested |
|---|---|
| Behaviour | default, open, closed, modal, non-modal |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation |
| Visual | default, open, closed, hover, focus |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/dialog.patch. Every hunk:
- OVL-23 —
DialogOverlaysets Base UI'sforceRender, so a dialog opened from inside another dialog or a sheet dims and blurs the page too; Base UI skips a nested backdrop otherwise. Operator decision D31 (Regent), 2026-10-30. - FOC-13 —
DialogContentpasses an overridableinitialFocusthat picks the first field or control, never the close ×, else the popup itself, so opening a dialog never lights up the ×. - OVL-13 —
DialogPortalreadsuseInternalThemeScope()and wraps its children in adisplay: contentselement carrying that class, so a nested theme scope survives the jump to<body>. The wrapper generates no box, so nothing about the backdrop's or the popup's fixed positioning changes.tooling/verify-portal-theme-scope.mjsinventories this host. - A11Y-9 —
DialogContentmirrors Base UI's livedata-base-ui-inertmarkers onto the nativeinertproperty throughuseModalInert, so a focus guard that hands focus to<body>cannot tab back into the background. The root now publishes itsmodalprop through a context, which is exactly the shape upstream's own Drawer uses, because native inert would also block the outside POINTER interaction thatmodal={false}and"trap-focus"keep. - BRD-1 —
DialogContentdraws a real 1pxborder border-borderinstead of upstream'sring-1 ring-foreground/10box-shadow outline. A border survives forced colours and matches Elevation; nothing else about the surface moves. - OVL-16 —
DialogContenttakessize?: "sm" | "default" | "md" | "lg" | "xl" | "fullscreen"(default"default") and reflects it asdata-size, the axisAlertDialogContentalready carries.defaultkeeps upstream'ssm:max-w-smunconditionally;sm,md,lgandxlstep it tomax-w-xs,max-w-lg,max-w-2xlandmax-w-5xlthroughdata-[size=…]. Approved by MK 23-09-2026 for a consumer gap;mdadded 03-10-2026 for share and settings dialogs.fullscreen(MK 27-10-2026, Regent product-cohesion W1-DS3, UXP-14) fills the viewport less a 16px inset (none on a phone),h-[calc(100dvh-2rem)], with the header and footer pinned and the body scrolling. - OVL-18 —
DialogFooterdrops upstream's-mx-4 -mb-4 rounded-b-xl border-t bg-muted/50 p-4band: it is as plain as the header, right-aligned fromsmand stacked below it. Its opt-in close isvariant="secondary". MK, 2026-09-25. - API-21 —
DialogBodyscrolls between the header and the footer; a popup holding one becomes a flex column capped atmax-h-[calc(100%-2rem)], and a dialog without one is untouched.closeLabelrenames the corner close control and the footer's opt-in close. - VOI-1 — both built-in close strings are the overridable
closeLabel, default "Close". - LAY-17 —
DialogContenttakesgrid-cols-1(oneminmax(0, 1fr)column) in place of the grid's implicitautocolumn, so a no-wrap child cannot widen the popup, andwrap-break-word, so a long unbroken word breaks inside it. While the corner × renders, the popup givesDialogHeaderpe-7, so a long title wraps before the × instead of running under it. MK, 2026-10-13. - TYP-19 —
DialogTitleisleading-snug, notleading-none, so a wrapped title's lines do not collide. MK, 2026-10-13. - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — the repository formatter reflows the file and
registry-stamp.mjswrites the provenance header the three-copy model needs; those hunks carry no decision of their own.