bx

Alert Dialog

A modal dialog that interrupts the user with important content and expects a response.

tsx
<AlertDialog>
  <AlertDialogTrigger for="delete">
    <Button variant="outline">Show Dialog</Button>
  </AlertDialogTrigger>
  <AlertDialogContent id="delete">
    <AlertDialogHeader>
      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. This will permanently delete your
        account and remove your data from our servers.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Continue</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

AlertDialog is a Dialog for irreversible decisions: deleting an account, discarding unsaved changes, cancelling an operation in progress. It is built on the Dialog parts and differs from Dialog in four ways:

  • The panel has role="alertdialog".
  • No × button in the corner.
  • Escape and a click on the backdrop do not close it: the user has to choose Cancel or Action.
  • AlertDialogCancel and AlertDialogAction are ready-made buttons that close the dialog. Cancel receives focus when the dialog opens.

Use it only for real destructive decisions; forms and edits belong in a regular Dialog.

Idiomatic composition:

  • AlertDialogTitle asks the question ("Are you absolutely sure?"). Phrasing it as a question is recommended.
  • AlertDialogDescription explains the consequences ("This action cannot be undone. This will permanently delete…"). Always state that the action is irreversible.
  • AlertDialogCancel is the neutral button (outline), typically on the left. Closes without acting.
  • AlertDialogAction is the primary (or destructive) button, on the right. It closes the dialog; attach your own handler to run the operation.

Installation

Terminal
$ bext ui add alert-dialog

Usage

tsx
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "@bext-stack/ui/primitives"
import { Button } from "@bext-stack/ui/primitives"

<AlertDialog>
  <AlertDialogTrigger for="delete-account">
    <Button variant="destructive">Delete account</Button>
  </AlertDialogTrigger>
  <AlertDialogContent id="delete-account">
    <AlertDialogHeader>
      <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone. This will permanently delete your
        account and remove your data from our servers.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Anatomy

<AlertDialog>
  ├── <AlertDialogTrigger for="alert-id">      // data-ui="open"
  │   └── <Button>Delete account</Button>
  └── <AlertDialogContent id="alert-id">        // role="alertdialog"
      ├── <AlertDialogHeader>
      │   ├── <AlertDialogTitle>...</AlertDialogTitle>
      │   └── <AlertDialogDescription>...</AlertDialogDescription>
      └── <AlertDialogFooter>
          ├── <AlertDialogCancel>Cancel</AlertDialogCancel>
          └── <AlertDialogAction>Delete</AlertDialogAction>

API Reference

AlertDialog

Prop Type Default Description
children ReactNode — The trigger and the content. Renders no element of its own.

AlertDialogTrigger

Prop Type Default Description
for string — Matches the id of the AlertDialogContent to open.
children ReactNode — Button that opens the dialog.
className string — Extra classes on the wrapping (inline-flex).

AlertDialogContent

Prop Type Default Description
id string — Matches AlertDialogTrigger.for.
ariaLabel string — Accessible name (aria-label) when there is no AlertDialogTitle. With a title, leave it out.
className string — Extra classes on the centered panel (role="alertdialog", aria-modal="true", max-w-lg). The panel has no corner close button.

AlertDialogHeader

Prop Type Default Description
className string — flex-col space-y-1.5 container for the title and description.

AlertDialogTitle

Prop Type Default Description
className string — <h2> text-lg font-semibold. Phrasing it as a question is recommended.

AlertDialogDescription

Prop Type Default Description
className string — <p> text-sm muted-foreground. Always state that the action is irreversible.

AlertDialogFooter

Prop Type Default Description
className string — flex-col-reverse on mobile → flex-row at sm+, justify-end. Cancel on the left, Action on the right.

AlertDialogCancel

Prop Type Default Description
children ReactNode — Label of the neutral button (typically "Cancel" or "Never mind"). Rendered as Button variant="outline" inside a data-ui="close" data-ui-cancel wrapper. It receives focus when the dialog opens.
className string — Extra classes on the button.

AlertDialogAction

Prop Type Default Description
children ReactNode — Label of the confirm button ("Continue", "Delete account"). Rendered as Button variant="default" inside a data-ui="close" wrapper. It has no variant prop: for a red button, use <DialogClose><Button variant="destructive"> instead (see the first example).
className string — Extra classes on the button.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-overlay AlertDialogContent (outer element) — Marks the overlay. The island toggles its hidden attribute.
data-ui="open" AlertDialogTrigger data-target="#id" Clicking opens the overlay and locks page scroll.
data-ui="close" Cancel + Action — Clicking closes the overlay and returns focus to the trigger.
data-ui-cancel AlertDialogCancel present Marks the button that receives focus on open.
data-ui="overlay-bg" backdrop — Present, but a click on it is ignored because the panel is an alertdialog.
data-state AlertDialogTrigger "open" | "closed" Set by the island, together with aria-haspopup="dialog", aria-expanded and aria-controls on the button inside.

Keyboard

Key Action
Enter / Space On the trigger: opens the dialog and focuses Cancel. Inside: activates the focused button.
Tab / Shift+Tab Moves between Cancel and Action and wraps around. Focus cannot leave the dialog.
Escape Does nothing: the dialog closes only from Cancel or Action.

Accessibility

AlertDialogContent sets role="alertdialog" and aria-modal="true" on the panel. The island points aria-labelledby at the AlertDialogTitle and aria-describedby at the AlertDialogDescription, so both are announced when the dialog opens.

Focus: on open, focus moves to the Cancel button, the least destructive action (or to the first focusable element when there is no AlertDialogCancel). Tab and Shift+Tab are trapped inside. On close, focus returns to the trigger.

Dismissal: Escape and a backdrop click are ignored. Always render a Cancel button, otherwise the user has no way out. Run the destructive operation only from the Action button.

Use sparingly: every AlertDialog interrupts the flow. Reserve it for real destructive decisions (permanent deletion, cancelling a batch, exiting without saving). For routine confirmation, a regular Dialog plus a confirmation toast after the action is enough.

Examples

Destructive action (red)

tsx
<AlertDialogFooter>
  <AlertDialogCancel>Cancel</AlertDialogCancel>
  <DialogClose>
    <Button variant="destructive">Delete</Button>
  </DialogClose>
</AlertDialogFooter>

Without description

tsx
<AlertDialogContent id="discard">
  <AlertDialogHeader>
    <AlertDialogTitle>Discard unsaved changes?</AlertDialogTitle>
  </AlertDialogHeader>
  <AlertDialogFooter>
    <AlertDialogCancel>Keep editing</AlertDialogCancel>
    <AlertDialogAction>Discard</AlertDialogAction>
  </AlertDialogFooter>
</AlertDialogContent>
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.