AlertDialog
Blocking confirmation dialog for destructive or critical actions. Built on Base UI's alert-dialog primitive with role="alertdialog" and a focus trap. A click outside never dismisses it — the user confirms, cancels, or presses Escape, which cancels.
pnpm add @tessinaui/uiWhen to use
- A decision the user must answer. The dialog blocks the page until they confirm or cancel; an outside click does nothing.
- A destructive or irreversible action the user may not expect, such as
deleting a project. Set
intent="error"so the action is coloured and Cancel takes initial focus. - A critical warning before data is lost or a change overwrites other
people's work (
intent="warning"). - Two actions, a confirm and a cancel. A third needs a reason you can state; past that the hierarchy stops reading and the footer crowds on a phone.
- A title that states the outcome ("Delete this project?", not "Are you sure?"), a description with the plain consequence (what is lost, whether it can be undone), and an action labelled with the title's verb ("Delete").
When not to use
- For a recoverable action. Do it, then offer a Toast with Undo. An alert in front of a reversible action trains people to dismiss alerts without reading them, which is what makes the irreversible one dangerous. The same tiering drives ToolCall's risk levels.
- For forms, lists or rich content. Use Modal, where an outside click is an acceptable dismissal and the decision is not critical.
- To inform without a decision. Use Alert inline in the page flow, or a Toast for a result that needs no reply.
- When destruction is the point of the flow the user chose. Emptying a trash
you deliberately opened confirms with an ordinary button;
intent="error"is for a destructive outcome the user may not be expecting. - With "OK", "Yes" or "No" labels, or raw error text. They force the reader back up to the title, and a screen-reader user who lands on the button hears nothing useful. Say what happened and what to do; keep any error code out of the title.
Installation
pnpm add @tessinaui/uiUsage
import {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogBody,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
} from "@tessinaui/ui";<AlertDialog intent="error">
<AlertDialogTrigger render={<Button intent="error">Delete</Button>} />
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>Examples
Default
A destructive confirmation with the error intent and an icon trigger.
Intents
intent drives the header icon and the default action-button colour — error, warning, success, and info.
Sizes
Four max-widths — sm, md, lg, and xl — to fit the message length.
Footer layout
AlertDialogFooter is responsive by default — full-width and stacked below sm, an end-aligned row from sm up. Pin either with layout="row" or layout="stacked".
Intent border
Opt in to a colour-matched popup border with showIntentBorder.
Destructive vs non-destructive
Intent decides more than colour: a destructive dialog focuses Cancel on open, per the APG rule to focus the least destructive action when undoing is impossible. A non-destructive one focuses the confirm instead.
Async confirm
Set busy while the mutation runs. The dialog stays open, the action shows a spinner, both buttons go inert, and Escape is vetoed — without the key ever being disabled.
Scrollable body
Put a long list of what the action affects in AlertDialogBody. It scrolls between a pinned header and footer, so the decision stays in view however long the list is.
Loading
AlertDialogSkeleton mirrors the popup shape while content loads.
On-color
On a coloured Surface the trigger takes the plate's ink and its focus ring from context — no prop needed. The dialog stays a standard panel: a portal opens it above the plate rather than on it, so its title, description and actions read exactly as they do on the page.
API Reference
AlertDialog (root)
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "md" | Max-width of the popup |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Corner radius — also inherited by AlertDialogAction and AlertDialogCancel |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Header icon and default action-button intent |
showIntentBorder | boolean | false | When true, the popup border picks up the intent colour. Opt-in — neutral border by default |
dir | "ltr" | "rtl" | inherited | Reading direction. Unset, read off the trigger when the dialog opens |
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Uncontrolled initial state |
onOpenChange | (open, event) => void | — | Fired when open state changes |
busy | boolean | false | A confirmed action is running: the action shows a spinner, both buttons go inert and every close request — Escape included — is vetoed until it clears |
AlertDialogContent
Inherits size, rounded, intent, showIntentBorder and dir from the root. Override any of them to scope the popup independently.
| Prop | Type | Default | Description |
|---|---|---|---|
initialFocus | "auto" | "action" | "cancel" | RefObject | "auto" | What takes focus on open. "auto" is Cancel for error / warning and the action otherwise |
finalFocus | RefObject | the trigger | What takes focus on close. Set it when the confirmed action removes the trigger |
AlertDialogHeader
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | intent icon | Custom leading icon next to the title |
showIcon | boolean | true | Render an icon — set to false for icon-less alerts |
AlertDialogFooter
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "responsive" | "row" | "stacked" | "responsive" | Stacked below sm and a row from sm up, always a row, or always stacked |
AlertDialogAction, AlertDialogCancel
Both render a Button. Pass any Button prop (size, rounded, loading, leadingIcon, etc.). Both auto-close the dialog on click.
AlertDialogActiondefaults tovariant="primary"and inherits the dialog'sintent.AlertDialogCanceldefaults tovariant="outline"andintent="none".- An
AlertDialogActionwhoseonClickreturns a promise does not close on that click — pair it withbusyand close the dialog yourself when the work settles.
AlertDialogTrigger, AlertDialogClose
Render a <button>. Use render={<CustomElement />} to swap in a Button or any element. Standard Base UI render-prop pattern.
AlertDialogSkeleton
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "lg" | "xl" | "md" | Max-width, matching the dialog it stands in for |
rounded | "none" | "sm" | "md" | "lg" | "full" | "md" | Corner radius of the panel and its bars |
showIcon | boolean | true | Render the leading icon placeholder |
lines | number | 2 | Description lines under the title |
actions | number | 2 | Buttons in the footer |
label | string | "Loading" | What assistive tech announces while the placeholder is up |
Sub-components
AlertDialogTitle, AlertDialogDescription, AlertDialogBody, AlertDialogBackdrop — all accept className and any HTML props.
Notes
- Built on
@base-ui/react/alert-dialog, with W3Crole="alertdialog"semantics. - The popup uses Base UI's
data-[starting-style]anddata-[ending-style]attributes for entry/exit animation. - The header icon background tints automatically per intent (
bg-error-lightetc.) for a soft callout.
Anatomy
<AlertDialog>
<AlertDialogTrigger>…</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle />
<AlertDialogDescription />
</AlertDialogHeader>
<AlertDialogBody /> {/* optional */}
<AlertDialogFooter>
<AlertDialogCancel />
<AlertDialogAction />
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>AlertDialogHeader automatically renders the icon for the current intent (or your custom icon). AlertDialogAction is intent-aware — it inherits the dialog's intent so destructive confirmations are coloured correctly without extra props.
Size
| Size | Max width |
|---|---|
"sm" | max-w-sm (24rem) |
"md" (default) | max-w-md (28rem) |
"lg" | max-w-lg (32rem) |
"xl" | max-w-xl (36rem) |
Intent
intent drives the header icon (auto) and the default colour of AlertDialogAction. The popup border stays neutral by default — opt in to a coloured border with showIntentBorder when the surrounding context doesn't already convey urgency.
| Value | Header icon | Default action button | Border with showIntentBorder |
|---|---|---|---|
"none" (default) | Info | bg-primary | border-border |
"error" | CircleX | bg-error | border-error |
"warning" | TriangleAlert | bg-warning | border-warning |
"success" | CircleCheck | bg-success | border-success |
"info" | Info | bg-info | border-info |
Rounded
| Value | CSS |
|---|---|
"none" | rounded-none |
"sm" | rounded-lg |
"md" (default) | rounded-xl |
"lg" | rounded-2xl |
"full" | rounded-3xl |
Footer layout
AlertDialogFooter accepts layout:
"responsive"(default) — full-width and stacked belowsm, a horizontal end-aligned row fromsmup."row"— always horizontal, end-aligned."stacked"— always full-width and vertical.
<AlertDialogFooter layout="stacked">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Power off</AlertDialogAction>
</AlertDialogFooter>Write AlertDialogCancel first in every layout. A stacked footer reverses the visual order, so the confirm sits on top, closest to the title, while Cancel stays first in the tab sequence.
Dismissal behaviour
- A click outside never dismisses. Unlike
Modal, the backdrop does nothing — thealertdialogrole asks for an explicit answer. - Escape closes, and counts as choosing Cancel.
AlertDialogAction,AlertDialogCanceland your ownAlertDialogCloseclose on click.- While
busy, every close request is vetoed — Escape included — until the action settles.
Controlled
const [open, setOpen] = React.useState(false);
<AlertDialog open={open} onOpenChange={setOpen}>
…
</AlertDialog>Custom close via AlertDialogClose
AlertDialogClose closes the dialog the moment it is clicked. Use it with render when the closing control is neither the confirm nor the cancel:
<AlertDialogClose render={<Button variant="ghost">Not now</Button>} />It does not wait for a click handler. For an action that must finish before the dialog closes, keep AlertDialogAction and set busy on the root — see Async confirm.
Accessibility
Pattern: WAI-ARIA APG alertdialog.
- The popup carries
role="alertdialog"andaria-modal="true", is named byAlertDialogTitleand described byAlertDialogDescription. Rendering the description is not optional — Base UI wiresaria-describedbyfrom that part, so omitting it silently drops an attribute the pattern requires. - Initial focus defaults to Cancel for
intent="error"/"warning"and to the action otherwise; override withinitialFocusonAlertDialogContent. The default is explicit rather than inherited from DOM order, so reordering the footer never moves focus onto the destructive button. - Escape closes, and must. It is equivalent to choosing Cancel. To hold the dialog open during an in-flight action use
busy, which vetoes the close in the handler — never disable the key. - Nothing is clipped on a short screen. At 30rem tall or less — a phone in landscape, or 400% zoom — the dialog scrolls as a whole, so its title and both actions stay reachable. A dialog with no
AlertDialogBodyscrolls that way whenever it is taller than the screen. - Outside clicks never dismiss. Base UI forces that for the
alertdialogrole; there is no prop to re-enable it. - While
busy, both buttons arearia-disabledrather thandisabled, so they stay in the tab sequence instead of vanishing mid-flight. - Cancel comes first in the tab order in every footer layout. A stacked footer reverses only the visual order, so the confirm sits on top while Cancel is still reached first.
- Focus returns to the trigger on close. When the confirmed action destroys the trigger — deleting the row that opened the dialog — pass
finalFocusso focus lands on a real element rather thanbody.
AT matrix: VoiceOver + Safari passed 2026-09-14 — Space on the trigger opens "web alert dialog", reading the title and description with focus on "Cancel, button"; Tab reaches "Delete, button" and wraps back; Escape returns to "Delete project, dialog pop up button". NVDA and JAWS UNTESTED (no Windows host, QA §5.6).
RTL
Pass dir="rtl" to the root. The header icon, text alignment, and footer button order all follow the reading direction. Tab order and initial focus do not change. Leave dir unset and the dialog follows the direction around the trigger that opened it; opened without a trigger, it follows the document.