Alert dialog

A confirmation that interrupts, with agent confirmation built in.

Example

Installation

pnpm dlx shadcn@latest add superzero11/uiset/alert-dialog

It lands in components/ui/alert-dialog.tsx, with UISet’s root (uiset). Wrap your app in <UISet> once, and every component takes its skin.

What UISet adds

The component is UI SDK’s own, with its parts, props and names, so everything below works as UI SDK documents it. UISet adds the look and checks it:

  • Styles in five skins, each in light and dark, from CSS variables you can override.
  • Text at 4.5:1 contrast and control edges and focus rings at 3:1, in every skin and theme, checked with axe in tests.
  • Haptic cues on phones, and sound cues when you set sound on <UISet>.
  • Examples that name every control, so agents can find it (see Agents below).

The API reference and accessibility notes below are UI SDK’s, from its docs for @ui-sdk/react (Apache-2.0). Alert dialog on ui-sdk.dev has more usage examples. Your coding agent can read the same page at node_modules/@ui-sdk/react/docs/components/alert-dialog.md.

API Reference

Alert Dialog shares Dialog's options, except modal, closeOnOutsidePress, and presentation. It is always modal, never closes on an outside press, and does not present as a sheet.

AlertDialog

  • container? (DocumentFragment | Element | null): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
  • open? (boolean): Whether the dialog is open. Pass it to control the dialog.
  • defaultOpen? (boolean): Whether the dialog starts open, when uncontrolled. Focus moves into it once it's on the page.
  • onOpenChangeComplete? (((open: boolean) => void)): Runs once the dialog has finished opening or closing, after any animation.
  • dismissible? (boolean): Whether Escape, an outside press and Android's back gesture can close the dialog. Close parts, triggers and the controller always can. Pass false while a form has unsaved changes, and ask in onDismissAttempt.
  • onDismissAttempt? (((details: DialogDismissAttemptDetails) => void)): Runs when a dismissal was refused, because dismissible is false, an outside press doesn't close it, or a Close part's action is running. Ask "Discard changes?" here.
  • persistentElements? ((string | Element)[] | (() => Element[])): Elements that stay usable while the dialog makes the page inert, like a chat launcher or a password manager's button. Elements, CSS selectors, or a function that returns elements, read when the dialog opens. Presses on them never close it.
  • pendingAnnouncement? (string): What screen readers hear once a Close part's action has been running for a second, so a slow action doesn't pass in silence. Set it in the page's language. An empty string says nothing.
  • debug? (boolean): Logs every state change with its reason to the console, in development builds.
  • onOpenChange? (((open: boolean, details: DialogOpenChangeDetails<AlertDialogOpenChangeReason>) => void)): Runs when the alert dialog wants to open or close. details.reason says why, details.cancel() stops it, and details.returnValue is the value of the Close part that closed it.
  • onPayloadChange? (((payload: unknown, details: ChangeDetails<AlertDialogPayloadChangeReason>) => void)): Runs when a trigger or a controller with another payload opens the alert dialog, before onOpenChange, or a controller's open({ payload }) gives the open one another payload. details.cancel() keeps the previous payload.
  • controller? (PopupController<any, AlertDialogOpenChangeReason, {}, any, DialogPayloadChangeReason>): Opens, closes and inspects the alert dialog from anywhere. Create one with createAlertDialogController() or createConfirm().

AlertDialogTrigger

  • disabled? (boolean): Stops the trigger from opening the dialog. A native button can't be focused or pressed, unless focusableWhenDisabled is set.
  • focusableWhenDisabled? (boolean): Keeps a disabled trigger in the tab order with aria-disabled instead of the native attribute, so it can host a tooltip that says why. Presses still do nothing.
  • payload? (unknown): Data for the content to render when this trigger opens the dialog, like the row it belongs to.
  • shortcut? (string): A keyboard shortcut that presses the trigger, like "mod+k". mod is Command on Apple platforms and Control elsewhere. Sets aria-keyshortcuts, and a Shortcut part inside shows it. A modal dialog pauses the page's shortcuts while it's open.
  • controller? (PopupController<any, AlertDialogOpenChangeReason, {}, any, DialogPayloadChangeReason>): Connects a trigger outside the alert dialog to the alert dialog with this controller.

AlertDialogBackdrop

Dims the page behind the dialog. Renders a div and accepts its props and render.

AlertDialogContent

  • container? (DocumentFragment | Element | null): Renders the popup into this element instead of in place, like a container inside a shadow root or an iframe. On web components, a CSS selector. Nested popups stay inside their parent.
  • initialFocus? (DialogInitialFocus): Where focus goes when the dialog opens. Opening with touch focuses the dialog itself, so no on-screen keyboard appears.
  • finalFocus? (PopupFocusTarget): Where focus goes when the dialog closes while focus is inside it.
  • keepMounted? (boolean): Whether the dialog stays in the page, hidden, while closed. For animation libraries that drive the exit from state.

AlertDialogTitle

The question. Renders an h2, and the content uses it as its accessible name.

AlertDialogDescription

What happens if you agree. Renders a p, and the content uses it as its accessible description.

AlertDialogClose

  • value? (string): Reported as returnValue in onOpenChange and by the controller's open(). createConfirm() resolves true for "confirm".
  • action? (((event: Event | undefined, details: ActionDetails) => unknown)): Runs when the part is pressed, before the dialog closes. A returned Promise keeps the dialog open while it runs: the part gets data-pending, aria-busy and aria-disabled, screen readers hear the dialog's pendingAnnouncement after a second, and nothing but the controller closes the dialog. It closes with reason "action" once the Promise resolves. A rejection keeps it open and puts the reason in the part's error state until the next press, or until the dialog opens again. details.source says how the part was pressed, for changes made after an await.

AlertDialogShortcut

  • shortcut? (string): The keys to show. Inside a trigger, its shortcut.

Confirm Options

The shared confirm() function uses these options when an OverlayHost renders the question. See Toasts and Confirms for the host setup. With your own dialog, createConfirm<Payload>() accepts the payload you choose instead.

  • title (string): The question, like "Delete this project?".
  • description? (string): What agreeing does.
  • confirmLabel? (string): The agreeing button's text.
  • cancelLabel? (string): The other button's text.

State

AlertDialogContent:

  • open (boolean): Whether the dialog is open. False while it animates out.
  • modal (boolean | "trap-focus"): The modal setting. Always true for an alert dialog.
  • nested (boolean): Whether the dialog was opened from inside another dialog.
  • nestedOpen (boolean): Whether a dialog opened from inside this one is open.
  • stack (number): How many dialogs are open above this one, nested inside it.
  • payload (Payload | undefined): The payload of the last open, from the trigger that opened it or a controller, typed as the controller's Payload.
  • pending (boolean): Whether a Close part's action is running.
  • presentation ("dialog"): Always "dialog": an alert dialog never presents as a sheet.

AlertDialogClose:

  • pending (boolean): Whether this part's action is running.
  • error (unknown): Why this part's last action failed, until it's pressed again or the dialog opens again.

AlertDialogShortcut:

  • text (string): The shortcut formatted for this platform: ⌘K on Apple platforms and Ctrl+K elsewhere. Empty without one.

Reasons

  • trigger-press (MouseEvent | KeyboardEvent | TouchEvent): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its touchend.
  • close-press (MouseEvent | KeyboardEvent | SubmitEvent): A Close part was pressed, or a form with method="dialog" was submitted.
  • escape-key (KeyboardEvent): Escape, or a submenu's close key (ArrowLeft in left-to-right layouts).
  • parent-close (none): The parent layer closed.
  • back (Event): Going back: the Android back button or gesture, or Backspace in an empty command input, which returns to the previous page.
  • action (MouseEvent | KeyboardEvent): A Close part's action finished.
  • shortcut (KeyboardEvent): A keyboard shortcut, like a trigger's shortcut.
  • imperative (none): A controller or api call, or a property set on a web component.

onPayloadChange(payload, details) runs before onOpenChange when the dialog opens with another payload. Calling controller.open({ payload }) on an open dialog updates the payload with the reason imperative.

  • trigger-press (MouseEvent | KeyboardEvent | TouchEvent): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its touchend.
  • shortcut (KeyboardEvent): A keyboard shortcut, like a trigger's shortcut.
  • imperative (none): A controller or api call, or a property set on a web component.

Controller

Use createAlertDialogController<Payload, Result>() for direct control. createConfirm<Payload>() returns a confirm function with its own controller.

  • isOpen() (() => boolean): Whether it's open. False as soon as it starts closing, while it still animates out.
  • setOpen(open, options?) ((open: boolean, options?: PopupChangeOptions<AlertDialogOpenChangeReason> | undefined) => boolean): Opens or closes it. Returns false when the change was canceled or it was already open or closed.
  • open(options?) ((options?: (PopupChangeOptions<DialogPayloadChangeReason> & { payload?: Payload | undefined; anchor?: Anchor | undefined; signal?: AbortSignal | undefined; }) | undefined) => Promise<...>): Opens it, and resolves once it closes, with why, how and any return value. Resolves exactly once and never rejects: on the next close from any source, when signal aborts, or with reason "unmount" when the component leaves the page.
  • close(options?) ((options?: (PopupChangeOptions<AlertDialogOpenChangeReason> & { returnValue?: Result | undefined; }) | undefined) => boolean): Closes it. returnValue is passed to the promise open() returned. Returns false when the close was canceled or it was already closed.
  • toggle(options?) ((options?: PopupChangeOptions<AlertDialogOpenChangeReason> | undefined) => boolean): Opens it when it's closed, and closes it when it's open. Returns false when the change was canceled.
  • getPayload() (() => Payload | undefined): The payload of the last open, from a controller or the trigger that opened it.
  • focus() (() => void): Moves focus to the trigger that opened it last, or the first trigger.

Accessibility

Give the dialog a title and a description, and put the least destructive answer first. It follows the WAI-ARIA alert dialog pattern.

The dialog uses the alertdialog role and keeps the rest of the page inert while it's open. Screen readers announce its title and description. Opening it focuses the first Close part, so put Cancel first. A touch open focuses the content instead, so no on-screen keyboard appears. Escape closes the dialog, but pressing outside never does. Dialog covers other initial and final focus targets.

These behaviors are covered by automated tests:

Alert dialog

  • Opens with focus on the least destructive action.
  • Is described by its message.
  • An outside press keeps it open.
  • Escape closes it and returns focus to the trigger.
  • Tab cycles between its actions.
  • The action reports and closes.

Runs in React, Vue and web components.

Agents

UI SDK’s agent bridge finds this component by its accessible name: give it a visible label or an aria-label, and an agent can read its state and call its actions without screenshots. Building with AI agents shows how to connect one.

This is agent.snapshot() for the example above, read live from this page. Change something in the example, then read it again: the values follow.

agent.snapshot()
[
  {
    "ref": "c8",
    "component": "alert-dialog",
    "actions": [
      "setOpen",
      "press"
    ],
    "label": "Remove member",
    "open": false
  }
]