Alert Dialog
A modal confirmation dialog with four semantic intents, backdrop lock, and deliberate cancel/confirm actions.
- Status
- Since
0.1.0- Accessibility pattern
- APG alert dialog (modal)
Last updated
Install
Add Alert Dialog from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/alert-dialogThe same command installs the registry items it composes: @vegastack/button, @vegastack/use-modal-inert.
Usage
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
<AlertDialog>
<AlertDialogTrigger
render={
<Button variant="outline" tone="destructive">
Delete project
</Button>
}
/>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete project?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction intent="destructive" onClick={handleDelete}>
Delete project
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>;Unlike Dialog, an alert dialog is not dismissable by clicking the backdrop — the user can
cancel with AlertDialogCancel or Esc, or confirm with AlertDialogAction. Reach for it
for destructive or irreversible confirmations; for everything else use Dialog.
Anatomy
AlertDialog is a compound component built on Base UI's AlertDialog. Compose the parts inside the
root:
<AlertDialog>
<AlertDialogTrigger render={<Button>Delete</Button>} />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Title</AlertDialogTitle>
<AlertDialogDescription>
Supporting description text.
</AlertDialogDescription>
</AlertDialogHeader>
{/* optional body content */}
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction intent="destructive">Confirm</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>AlertDialog— the root; owns open/close state (open/defaultOpen/onOpenChange). Always modal — focus is trapped, page scroll is locked, and the backdrop does not dismiss.AlertDialogTrigger— the control that opens the dialog (data-slot="alert-dialog-trigger"). Passrenderto compose it with aButton.AlertDialogContent— the centered popup (data-slot="alert-dialog-content"). Composes Base UI's Portal + Backdrop + Viewport + Popup and animates enter/exit. No top-right close button, and nointent— the confirm button owns that.AlertDialogHeader— groups the title and description (data-slot="alert-dialog-header").AlertDialogTitle— the dialog's accessible name (data-slot="alert-dialog-title",<h2>). Wired to the popup viaaria-labelledby.AlertDialogDescription— supporting text (data-slot="alert-dialog-description",<p>). Wired viaaria-describedby.AlertDialogFooter— the action row at the bottom (data-slot="alert-dialog-footer").AlertDialogCancel— closes without confirming (data-slot="alert-dialog-cancel"). Neutral outline tint.AlertDialogAction— the confirm button (data-slot="alert-dialog-action"). Closes the dialog; wire confirm work throughonClick. Carries theintenttint.
Not backdrop-dismissable: there is intentionally no close (
X) button and backdrop clicks do not dismiss. The user can cancel with Cancel or Esc, or choose the confirm Action — that's what separates an alert dialog from a regularDialog.
Examples
Intents
Set intent on AlertDialogAction to tint the confirm button: default, destructive, success,
and warning.
AlertDialogContent deliberately has no intent. It used to accept one that wrote a
data-intent attribute and nothing else, while the visible colour came from AlertDialogAction —
two props for one concept, one of them inert. AlertDialogAction intent is the single owner; it maps
to the shared Button matrix (variant="outline" plus the matching tone), so a destructive
confirm is an outline button, never a solid red one.
Scrollable content
When the body is long, drop a scrolling region (overflow-y-auto) between the header and footer. The
popup caps its own height (max-h-[calc(100dvh-…)]) and keeps the header and footer pinned while the
middle scrolls.
API Reference
AlertDialogContent
AlertDialogContent 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 AlertDialogContent
| Attribute | Values |
|---|---|
data-slot | "alert-dialog-backdrop" | "alert-dialog-content" | "alert-dialog-viewport" |
AlertDialogAction
| Prop | Type | Default | Description |
|---|---|---|---|
intent | AlertDialogIntent | "default" | Semantic tint of the confirm button — the single owner of the confirmation's severity. |
loading | boolean | false | Shows a spinner and marks the confirm button busy while an async confirm is pending — wired
straight through to the underlying Button's loading prop (spinner, aria-busy, and a
real disabled control). Because the rendered button becomes genuinely disabled, a click
while loading never reaches Base UI's Close click handler, so the dialog stays open until
you flip loading back to false (typically in the onClick handler's finally). |
Data attributes and CSS variables on AlertDialogAction
| Attribute | Values |
|---|---|
data-intent | mirrors a prop or state value |
data-slot | "alert-dialog-action" |
AlertDialog, AlertDialogTrigger, AlertDialogHeader, AlertDialogFooter,
AlertDialogTitle, AlertDialogDescription, and AlertDialogCancel add no props of their
own — they accept everything the underlying Base UI part or element accepts (className,
render, ARIA attributes, event handlers, …).
Accessibility
- The popup renders with
role="alertdialog"+aria-modal="true".AlertDialogTitleandAlertDialogDescriptionare 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. VegaStack
mirrors Base UI's live outside-tree markers to native
inert, shared with Dialog and Sheet through reference-counted ownership, so focus cannot restart in page chrome after a transient<body>hand-off. Page scroll is locked. - The backdrop is non-dismissable by design — closing requires
AlertDialogCancel,AlertDialogAction, or Esc as a cancel request, so a confirmation is never dismissed by accident. - Every interactive control is a
Buttonand followsButton's focus treatment: each keeps the global 2px:focus-visiblering (outline-ring), and the borderedAlertDialogCancel(outlinevariant) additionally re-colors its border with theringtoken (focus-visible:border-ring/(--alpha-tint-border)) — neveroutline: none.
| Key | Action |
|---|---|
| Enter / Space | Activate the focused trigger to open the dialog. |
| Tab / Shift + Tab | Move focus between Cancel and Action — trapped within the open dialog. |
| Esc | Close the dialog (treated as cancel). |
| Contract | States tested |
|---|---|
| Behaviour | default, disabled, error, loading, open, success |
| Accessibility | busy, described, focus-visible, labeled, semantic-html |
| Visual | default, loading, error, success |