Dialog
A modal overlay — five sizes, a header/footer layout, focus trapping, and animated enter/exit.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (modal)
Last updated
Install
Add Dialog from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/dialogThe same command installs the registry items it composes: @vegastack/icon-button, @vegastack/use-modal-inert.
Usage
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
<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="outline">Cancel</Button>} />
<Button variant="soft" tone="destructive">
Delete project
</Button>
</DialogFooter>
</DialogContent>
</Dialog>;Anatomy
Dialog is a compound component built on Base UI's Dialog. Every exported part, with the
data-slot it renders (generated from the canonical source):
Compose the parts inside the root:
<Dialog>
<DialogTrigger render={<Button>Open</Button>} />
<DialogContent size="md">
<DialogHeader>
<DialogTitle>Title</DialogTitle>
<DialogDescription>Supporting description text.</DialogDescription>
</DialogHeader>
{/* body content */}
<DialogFooter>
<DialogClose render={<Button variant="outline">Cancel</Button>} />
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>Dialog— the root; owns open/close state (open/defaultOpen/onOpenChange). Modal by default — focus is trapped and page scroll is locked.DialogTrigger— the control that opens the dialog (data-slot="dialog-trigger"). Passrenderto compose it with aButton.DialogContent— the centered popup (data-slot="dialog-content"). Composes Base UI's Portal + Backdrop + Viewport + Popup, owns thesize, animates enter/exit, and renders the top-right close button.DialogHeader— groups the title and description (data-slot="dialog-header").DialogTitle— the dialog's accessible name (data-slot="dialog-title",<h2>). Wired to the popup viaaria-labelledby.DialogDescription— supporting text (data-slot="dialog-description",<p>). Wired viaaria-describedby.DialogFooter— the action row at the bottom (data-slot="dialog-footer").DialogClose— closes the dialog (data-slot="dialog-close-action"). Passrenderto compose it with aButton(e.g. a "Cancel" action).
Mobile: Dialog stays a centered modal at every breakpoint. There is intentionally no mobile-drawer variant — a full-screen Drawer/Sheet is a separate component.
Examples
Sizes
DialogContent takes a size prop controlling the popup's max-width: xs, sm, default, lg, and full.
Close button
By default DialogContent renders a top-right close (X) button. Pass showCloseButton={false} to remove it — leaving an explicit footer action as the only way out (useful for deliberate confirmations).
<DialogContent showCloseButton={false}>{/* … */}</DialogContent>The close button's accessible label defaults to "Close". Override it with closeLabel when a more specific label reads better for screen readers.
<DialogContent closeLabel="Dismiss dialog">{/* … */}</DialogContent>Scrollable content
The popup caps its height at max-h-[calc(100dvh-2rem)]. When the body is taller than the viewport, give the body region overflow-y-auto so it scrolls while the header and footer stay pinned.
Playground
Pick a size and toggle the close button, open the dialog, then copy the generated JSX.
<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="outline">Cancel</Button>} />
<Button variant="soft" tone="destructive">Delete</Button>
</DialogFooter>
</DialogContent>
</Dialog>API Reference
DialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
closeLabel | string | "Close" | Accessible label for the close button. |
placement | "center" | "top" | 'center' | Vertical placement of the popup (Wave 2). center (default) is the modal
position; top anchors the popup near the viewport top — the COMPOSER
posture (quick-create dialogs, task capture) where the eye starts and
follow-up typing happens. |
showCloseButton | boolean | true | Render the top-right close (X) button. |
size | DialogContentSize | "md" | Max-width size of the centered popup. |
Data attributes and CSS variables on DialogContent
| Attribute | Values |
|---|---|
data-placement | mirrors a prop or state value |
data-size | mirrors a prop or state value |
data-slot | "dialog-backdrop" | "dialog-close" | "dialog-content" | "dialog-viewport" |
DialogTitleBar
DialogTitleBar adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
Data attributes and CSS variables on DialogTitleBar
| Attribute | Values |
|---|---|
data-slot | "dialog-title-bar" |
DialogTitleBar is the optional window-chrome header row for desktop-style dialogs. Compose the
accessible DialogTitle inside it; the title bar does not replace the dialog's accessible name.
Dialog, DialogTrigger, DialogTitle, DialogDescription, and
DialogClose add no props of their own — each accepts everything the
corresponding Base UI Dialog
part accepts (Root, Trigger, Title, Description, Close): open /
defaultOpen / onOpenChange on the root, render on the trigger and close,
plus className and ref. DialogHeader and DialogFooter are plain layout
wrappers that accept standard <div> props.
Accessibility
- The popup renders with
role="dialog"+aria-modal="true".DialogTitleandDialogDescriptionare auto-wired as itsaria-labelledby/aria-describedby— always include a title. - Focus is trapped inside the popup while open and restored to the trigger on close. For the default
modal, VegaStack mirrors Base UI's live outside-tree markers to native
inert; this keeps focus contained even when the browser transiently hands it to<body>, and preserves nested portal stacking through reference-counted ownership. Page scroll is locked.modal={false}andmodal="trap-focus"keep their Base UI outside-interaction contracts and do not add native inert. - The close button is a native button with an accessible label (
closeLabel, default"Close"); meaning of theXicon is carried by that label. - Controls composed with
Button(trigger, footer actions) carryButton's own:focus-visiblering. The top-rightXclose button keeps the browser's default focus outline and changes colour on hover/focus — it never setsoutline: none.
| Key | Action |
|---|---|
| Enter / Space | Activate the focused trigger to open the dialog. |
| Tab / Shift + Tab | Move focus between controls — trapped within the open dialog. |
| Esc | Close the dialog. |
The states the verification lanes exercise for this item (generated from component-contracts.json):
| Contract | States tested |
|---|---|
| Behaviour | default, error, open |
| Accessibility | described, focus-visible, labeled, semantic-html |
| Visual | default, hover, error |