Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Dialog

A modal overlay — five sizes, a header/footer layout, focus trapping, and animated enter/exit.

Status
stable
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/dialog

The 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):

Dialog
DialogClose — data-slot="dialog-close-action"
DialogContent — data-slot="dialog-backdrop" | "dialog-close" | "dialog-content" | "dialog-viewport"
DialogDescription — data-slot="dialog-description"
DialogFooter — data-slot="dialog-footer"
DialogHeader — data-slot="dialog-header"
DialogTitle — data-slot="dialog-title"
DialogTitleBar — data-slot="dialog-title-bar"
DialogTrigger — data-slot="dialog-trigger"

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"). Pass render to compose it with a Button.
  • DialogContent — the centered popup (data-slot="dialog-content"). Composes Base UI's Portal + Backdrop + Viewport + Popup, owns the size, 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 via aria-labelledby.
  • DialogDescription — supporting text (data-slot="dialog-description", <p>). Wired via aria-describedby.
  • DialogFooter — the action row at the bottom (data-slot="dialog-footer").
  • DialogClose — closes the dialog (data-slot="dialog-close-action"). Pass render to compose it with a Button (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

PropTypeDefaultDescription
closeLabelstring"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.
showCloseButtonbooleantrueRender the top-right close (X) button.
sizeDialogContentSize"md"Max-width size of the centered popup.

Data attributes and CSS variables on DialogContent

AttributeValues
data-placementmirrors a prop or state value
data-sizemirrors 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

AttributeValues
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". DialogTitle and DialogDescription are auto-wired as its aria-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} and modal="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 the X icon is carried by that label.
  • Controls composed with Button (trigger, footer actions) carry Button's own :focus-visible ring. The top-right X close button keeps the browser's default focus outline and changes colour on hover/focus — it never sets outline: none.
KeyAction
Enter / SpaceActivate the focused trigger to open the dialog.
Tab / Shift + TabMove focus between controls — trapped within the open dialog.
EscClose the dialog.

The states the verification lanes exercise for this item (generated from component-contracts.json):

ContractStates tested
Behaviourdefault, error, open
Accessibilitydescribed, focus-visible, labeled, semantic-html
Visualdefault, hover, error

Do / Don't

Do
Always include a DialogTitle so the dialog has an accessible name, and put primary actions in the DialogFooter.
Don't
Use a Dialog for a full-screen, scroll-heavy mobile experience — use a Drawer/Sheet instead.

On this page