Alert dialog
A centred confirmation dialog that interrupts to ask a question. A trigger opens a portalled, modal panel that traps focus and blurs the page — composed from a header, a body, and a footer holding an explicit Cancel and Action. Unlike a Sheet or Dialog, a click outside never dismisses it: the user must answer.
Basic
An AlertDialog.Trigger opens the AlertDialog.Content. Compose the trigger and the footer buttons from Button through render so they inherit the button look, hover, and focus. The footer pairs an AlertDialog.Cancel (which closes without committing) with an AlertDialog.Action (which confirms). Initial focus lands on Cancel — the safe choice — so a stray Enter never confirms.
"use client";
import { AlertDialog, Button } from "@stridge/noctis";
export default function AlertDialogBasic() {
return (
<AlertDialog.Root>
<AlertDialog.Trigger render={<Button variant="outline">Leave page</Button>} />
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Discard changes?</AlertDialog.Title>
<AlertDialog.Description>You have unsaved edits on this page.</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Body>If you leave now, your changes will be lost.</AlertDialog.Body>
<AlertDialog.Footer>
<AlertDialog.Cancel render={<Button variant="secondary">Keep editing</Button>} />
<AlertDialog.Action render={<Button variant="primary">Leave</Button>} />
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Severity tone
Set tone on AlertDialog.Content — neutral (default), danger, or warning — to mark the confirmation's severity. Tone paints a restrained, status-role cue: a leading AlertDialog.Icon recoloured to the danger/warning status colour, a colour-independent severity signal (the icon shape, not just the colour) that respects two-accent discipline — never an accent or a red-washed panel. The title stays full-contrast foreground, and the action's own danger look still comes from composing a <Button variant="danger">. info/success are intentionally absent: an alert dialog is an action gate, not a notice — route those to a Dialog.
"use client";
import { AlertDialog, Button, Icon } from "@stridge/noctis";
import { TriangleAlert } from "lucide-react";
export default function AlertDialogDestructive() {
return (
<AlertDialog.Root>
<AlertDialog.Trigger render={<Button variant="danger">Delete account</Button>} />
{/* tone="danger" recolours the leading icon to the danger status role — a colour-independent severity signal. */}
<AlertDialog.Content tone="danger">
<AlertDialog.Header>
<AlertDialog.Icon>
<Icon icon={TriangleAlert} />
</AlertDialog.Icon>
<AlertDialog.Title>Delete account?</AlertDialog.Title>
<AlertDialog.Description>This action is permanent and cannot be undone.</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Body>
Your projects, settings, and history will be erased. Initial focus stays on Cancel, so an accidental Enter
never confirms.
</AlertDialog.Body>
<AlertDialog.Footer>
<AlertDialog.Cancel render={<Button variant="secondary">Cancel</Button>} />
<AlertDialog.Action tone="danger" render={<Button variant="danger">Delete forever</Button>} />
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
A warning tone fits a cautionary, non-destructive gate — proceeding past a soft block rather than erasing something.
"use client";
import { AlertDialog, Button, Icon } from "@stridge/noctis";
import { TriangleAlert } from "lucide-react";
export default function AlertDialogWarning() {
return (
<AlertDialog.Root>
<AlertDialog.Trigger render={<Button variant="outline">Publish</Button>} />
{/* warning tone for a cautionary (non-destructive) gate — the icon takes the warning status colour. */}
<AlertDialog.Content tone="warning">
<AlertDialog.Header>
<AlertDialog.Icon>
<Icon icon={TriangleAlert} />
</AlertDialog.Icon>
<AlertDialog.Title>Publish with unresolved comments?</AlertDialog.Title>
<AlertDialog.Description>Three comments are still open on this draft.</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Body>
Reviewers may expect those resolved first. You can publish anyway and address them later.
</AlertDialog.Body>
<AlertDialog.Footer>
<AlertDialog.Cancel render={<Button variant="secondary">Keep editing</Button>} />
<AlertDialog.Action render={<Button variant="primary">Publish anyway</Button>} />
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Async confirmation
Real confirmations are asynchronous — the action hits the server. Surface Base UI's actionsRef on AlertDialog.Root for an imperative handle, and confirm with a plain Button (not Action) so the dialog doesn't close optimistically: hold a pending state, render the button as loading, disable Cancel while in flight, then call actionsRef.current.close() once the request resolves (keep it open and show the error on failure). Focus stays trapped throughout.
"use client";
import { AlertDialog, Button, Icon } from "@stridge/noctis";
import { TriangleAlert } from "lucide-react";
import { useRef, useState } from "react";
export default function AlertDialogAsyncConfirm() {
// `actionsRef` is the imperative handle: keep the dialog open while the request runs, then close it.
const actions = useRef<AlertDialog.Root.Actions | null>(null);
const [pending, setPending] = useState(false);
async function confirm() {
setPending(true);
await new Promise((resolve) => setTimeout(resolve, 1400)); // pretend to call the server
setPending(false);
actions.current?.close();
}
return (
<AlertDialog.Root actionsRef={actions}>
<AlertDialog.Trigger render={<Button variant="danger">Delete account</Button>} />
<AlertDialog.Content tone="danger">
<AlertDialog.Header>
<AlertDialog.Icon>
<Icon icon={TriangleAlert} />
</AlertDialog.Icon>
<AlertDialog.Title>Delete account?</AlertDialog.Title>
<AlertDialog.Description>This permanently deletes your account and data.</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Body>
The request runs in the background; the dialog stays open and keeps focus until it settles.
</AlertDialog.Body>
<AlertDialog.Footer>
{/* Cancel is disabled while the request is in flight. */}
<AlertDialog.Cancel
render={
<Button variant="secondary" disabled={pending}>
Cancel
</Button>
}
/>
{/* A plain Button (not Action/Close) so the dialog does not close until the promise resolves. */}
<Button variant="danger" loading={pending} onClick={confirm}>
{pending ? "Deleting…" : "Delete forever"}
</Button>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Type to confirm
For the highest-stakes, irreversible actions, gate the destructive Action behind typing the resource's name — forcing recognition over reflex (GitHub's Danger Zone, Vercel). Keep the action disabled until a controlled input matches the required phrase. Shipped here as a composition; promote it to a part once it proves repetitive.
"use client";
import { AlertDialog, Button, Icon, Input } from "@stridge/noctis";
import { TriangleAlert } from "lucide-react";
import { useId, useState } from "react";
const PROJECT = "acme-prod";
export default function AlertDialogTypeToConfirm() {
const [value, setValue] = useState("");
const fieldId = useId();
const matches = value === PROJECT;
return (
<AlertDialog.Root onOpenChange={() => setValue("")}>
<AlertDialog.Trigger render={<Button variant="danger">Delete project</Button>} />
<AlertDialog.Content tone="danger">
<AlertDialog.Header>
<AlertDialog.Icon>
<Icon icon={TriangleAlert} />
</AlertDialog.Icon>
<AlertDialog.Title>Delete this project?</AlertDialog.Title>
<AlertDialog.Description>This permanently deletes the project and everything in it.</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Body>
<div className="flex flex-col gap-2">
<label htmlFor={fieldId} className="text-small text-secondary">
Type <span className="font-mono text-foreground">{PROJECT}</span> to confirm
</label>
<Input.Root>
<Input.Control
id={fieldId}
value={value}
onValueChange={setValue}
autoComplete="off"
aria-label={`Type ${PROJECT} to confirm`}
className="font-mono"
/>
</Input.Root>
</div>
</AlertDialog.Body>
<AlertDialog.Footer>
<AlertDialog.Cancel render={<Button variant="secondary">Cancel</Button>} />
{/* The destructive action stays disabled until the typed value matches — recognition, not reflex. */}
<AlertDialog.Action
disabled={!matches}
render={
<Button variant="danger" disabled={!matches}>
Delete project
</Button>
}
/>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Multiple actions
The footer lays out its buttons by flex — stacked on a narrow screen, a row aligned to the inline-end from 40rem up — so it holds two or three actions without a new part. Order them so the primary sits at the inline-end, Cancel before it, and any secondary between.
"use client";
import { AlertDialog, Button } from "@stridge/noctis";
export default function AlertDialogSecondaryAction() {
return (
<AlertDialog.Root>
<AlertDialog.Trigger render={<Button variant="outline">Close without saving</Button>} />
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Save changes before closing?</AlertDialog.Title>
<AlertDialog.Description>You have unsaved edits in this document.</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Body>Save them, discard them, or keep editing.</AlertDialog.Body>
{/*
* The footer lays out N actions by flex (stacked on mobile, a row from 40rem up). Order them so
* the primary sits at the inline-end, Cancel before it, and any secondary between.
*/}
<AlertDialog.Footer>
<AlertDialog.Cancel render={<Button variant="secondary">Keep editing</Button>} />
<AlertDialog.Action render={<Button variant="ghost">Discard</Button>} />
<AlertDialog.Action render={<Button variant="primary">Save</Button>} />
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
);
}
Writing the copy
A confirmation is only as safe as its words. Be specific, not generic: name the resource and the count ("Delete 3 issues?" over "Are you sure?"). State the consequence first in the body, and close an irreversible one with "This cannot be undone." Make the button labels summarize the outcome — Delete forever / Keep editing, not OK / Cancel — and keep Cancel literally "Cancel" for consistency. Where you can, prefer an undo to a confirmation: an interruption you don't need is friction. (Sources: NN/g, Vercel Geist, WAI-ARIA APG.)
Anatomy
Compose an alert dialog from its parts. AlertDialog.Root owns the open state — it accepts every Base UI AlertDialog.Root prop, including open, defaultOpen, onOpenChange, and actionsRef (the imperative close()/unmount() handle for async flows). Always modal: focus is trapped, the page is scroll-locked, the rest of the document is inert, and — being a confirmation — it is dismissed only by Cancel, Action, or Esc, never an outside click.
AlertDialog.Root— owns the open state and the shared Cancel-button ref; renders no element of its own. SurfacesactionsRef.AlertDialog.Trigger— opens the dialog. Style it directly or compose aButtonthroughrender.AlertDialog.Content— the common composition: portal, backdrop, centring viewport, and panel in one. Props:tone(neutraldefault,danger,warning),size(mddefault,sm),backdropClassName, and anyAlertDialog.Popupprop (initialFocus,finalFocus). Reach forAlertDialog.Portal+Backdrop+Viewport+Popupdirectly only when you need to customize the portal or backdrop wiring.AlertDialog.Popup— the centred panel surface. Settone/sizehere, andinitialFocus/finalFocusto control where focus lands (defaults to Cancel) and returns (defaults to the trigger; set it when the trigger has unmounted).AlertDialog.Header— the panel's top region for theIcon,Title, andDescription, separated from the body by a divider. With an icon it becomes a gutter grid (icon at the inline-start, text beside it).AlertDialog.Icon— the leading severity glyph. Wrap a NoctisIcon; it is decorative (aria-hidden) and recolours by the paneltone.AlertDialog.Body— the middle region holding the message the user confirms; it reads at full contrast (never the dimmest text in the panel).AlertDialog.Footer— the panel's bottom region, pinned to the base, holding theCancel/Actionpair (and an optional secondary action).AlertDialog.Title+AlertDialog.Description— the panel's accessible name and supporting copy, linked to the popup viaaria-labelledbyandaria-describedby(the described element is the consequence message).AlertDialog.Cancel— closes the dialog without committing, and is the popup's default initial-focus target. Renders a bare button, so it composes with anyButtonthroughrender. Enter on it cancels — never confirms.AlertDialog.Action— confirms the prompt and closes. Takes an explicittone; composes with aButtonthroughrender.AlertDialog.Close— the low-level dismiss theCancel/Actionparts are built on, for a custom action.
Every rendered part carries a data-slot (noctis-alert-dialog-trigger, noctis-alert-dialog-backdrop, noctis-alert-dialog-viewport, noctis-alert-dialog-popup, noctis-alert-dialog-close, noctis-alert-dialog-icon, noctis-alert-dialog-cancel, noctis-alert-dialog-action, noctis-alert-dialog-header, noctis-alert-dialog-body, noctis-alert-dialog-footer, noctis-alert-dialog-title, noctis-alert-dialog-description) for host-side styling — the popup also carries data-tone and data-size (and data-nested-dialog-open when stacked). Pair it with the Base UI state attributes (data-open, data-closed, data-starting-style, data-ending-style). The popup renders through Surface at elevated elevation, bordered, with the modal shadow, and the backdrop blurs the page — matching the Dialog family.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | On the trigger: open the dialog and move focus to the Cancel button. |
| Tab / Shift + Tab | Move focus to the next / previous element, trapped within the open panel. |
| Enter / Space (on Cancel) | Cancel and close — the focused default never confirms. |
| Esc | Close the dialog without committing, and return focus to the trigger (or finalFocus). |
A click on the dimmed backdrop does not close an alert dialog — a confirmation must be answered through Cancel or Action. Content behind the panel stays inert and unreachable while the dialog is open. The header icon, footer alignment, and layout mirror under RTL by construction.
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every alert dialog in that region retunes — e.g. .app { --noctis-alert-dialog-popup-max-inline-size: 32rem; } widens the panel beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI's AlertDialog list just the props they pass through. Expand a row for the full type and description.
AlertDialog.Root
No props of its own — forwards to the underlying Base UI part.