Components / overlays
AlertDialog
This component was flagged as possibly broken in production before the first release: it had no CSS of its own and leaned on rules that only apply to a native <dialog>, which it is not. It rendered unpositioned and undimmed with a live focus trap. Fixed, and the specimen now proves it renders.
Summary
A modal that interrupts to confirm something consequential — deleting, publishing, discarding.
| APG pattern | Alert Dialog |
| Built on | Radix |
| Status | stable |
| Since | 0.1.0 |
<AlertDialog open={open} onOpenChange={setOpen}
title="Delete Northlight?"
description="The case study and its 12 images are removed permanently."
confirmLabel="Delete permanently" onConfirm={destroy} />
Anatomy
A portalled panel carrying role="alertdialog", over its own backdrop.
| Part | Element | Required |
|---|---|---|
body | the caller's | the caller's |
close | the caller's | the caller's |
description | <radixalertdialog.description> | yes |
footer | <div> | yes |
header | the caller's | the caller's |
label | <span> | yes |
title | <radixalertdialog.title> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
open | boolean | none | Whether the dialog is showing. |
onOpenChange | (open: boolean) => void | none | Called when the reader dismisses it with Escape or Cancel. |
title | React.ReactNode | required | The question, naming the thing — "Delete Northlight?" rather than "Are you sure?". It is the accessible name. |
description | React.ReactNode | required | What is lost and whether it comes back. Required, because role="alertdialog" announces it on open and an alert dialog with nothing to announce should have been a Dialog. |
confirmLabel | React.ReactNode | required | Repeat the verb: "Delete permanently", never "OK". |
cancelLabel | React.ReactNode | Cancel | The way out. Focus lands here on open, so Enter on arrival cancels rather than destroys. |
onConfirm | () => void | none | Called when the reader confirms. The dialog does not close itself — the caller decides what happens after the work succeeds or fails. |
intent | negative · accent · positive · warning | negative | The confirm button's intent. negative by default, because that is what an alert dialog is usually for. |
trigger | React.ReactNode | none | The control that opens it, wired via asChild. |
States
| State | Expressed as | |
|---|---|---|
| Pressed | :active | |
| Disabled | :disabled | |
| Hover | :hover | |
| Open or closed | data-state |
Behavior
Three differences from Dialog, all deliberate. role="alertdialog" so the message is announced on open rather than waiting to be read. Focus lands on Cancel rather than the destructive action. And there is no dismiss-by-outside-click — a decision this size should not be closable by a stray tap.
Escape still closes, because a reader must always have a way out that is not the destructive one.
Accessibility
role="alertdialog" announces the description on open. That is the entire reason it is not a Dialog: the reader hears what is at stake before they can act.
Focus on Cancel is the safety. Enter on arrival must not destroy anything.
Keyboard
| Key | Result |
|---|---|
Escape | Closes — a reader must always have a way out that is not the destructive one |
Tab | Cycles inside the panel |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-modal | always true |
role | always alertdialog |
Focus
Focus lands on Cancel, not on the confirm button. Enter on arrival must not destroy anything. There is no dismiss-by-outside-click; focus returns to the trigger on close.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It takes the touch floor under (pointer: coarse) — the visual size is unchanged on a desktop pointer.
Tested
test/apg/batch-a.test.jsxtest/apg/partials-sweep.test.jsxtest/browser/keyboard.spec.mjstest/browser/upstream-natives.spec.mjs
Known gaps
- 2 upstream refs — APG's own page or markup, not the pattern
Appearance
| Token | Used for |
|---|---|
--se-accent | intent |
--se-bg | background, border-color |
--se-border | border, border-color |
--se-border-width | border, border-width |
--se-control-h | block-size, inline-size |
--se-control-h-lg | block-size, inline-size |
--se-control-h-sm | block-size, inline-size |
--se-duration-instant | transition |
--se-duration-medium | animation |
--se-ease-decelerate | animation |
--se-ease-standard | transition |
--se-layer-backdrop | z-index |
--se-layer-overlay | z-index |
--se-leading-body | line-height |
--se-leading-heading | line-height |
--se-mix-down | background, border-color |
--se-mix-hover | background, border-color |
--se-mix-line | border-color |
--se-mix-press | background |
--se-mix-shade | background, border-color |
--se-mix-soft | background |
--se-negative | intent |
--se-positive | intent |
--se-radius-lg | border-radius |
--se-radius-md | border-radius |
--se-shadow-overlay | box-shadow |
--se-space-2 | gap |
--se-space-3 | gap, padding, padding-inline |
--se-space-4 | gap, padding-inline |
--se-space-5 | padding, padding-block-end, padding-block-start, padding-inline |
--se-surface | background |
--se-surface-sunk | background |
--se-target-min | block-size, inline-size, min-block-size |
--se-text | background, color, intent |
--se-text-disabled | color |
--se-text-md | font-size |
--se-text-muted | color |
--se-text-on-intent | color |
--se-text-sm | font-size |
--se-text-xs | font-size |
--se-tracking-heading | letter-spacing |
--se-weight-semibold | font-weight |
The portalled variant carries the same surface as the native dialog by hand, because there is no shared floating surface to inherit — one of the four that have already drifted, recorded in 3.5.
Motion
Entry over --se-duration-medium, gated so axe never measures it mid-fade.
Content
The confirm label repeats the verb. Delete permanently tells a reader who arrived mid-sentence what they are agreeing to; OK and Confirm do not.
The description says what is lost and whether it can come back.
Usage
Use Dialog for anything the reader can undo, and nothing at all for something reversible with a toast.
One question per alert dialog. If there are two decisions, the first one has not been made yet.
Notes
Radix is permitted here — Alert Dialog is one of the six patterns on the closed list, and it supplies the focus scope and the portal.
This component was flagged as possibly broken in production before the first release: it had no CSS of its own and leaned on rules that only apply to a native <dialog>, which it is not. It rendered unpositioned and undimmed with a live focus trap. Fixed, and the specimen now proves it renders.