# Alert dialog

A modal question that needs an answer before the page continues, such as confirming a deletion.

- **Import:** `import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger } from "@booleanpress/ui/alert-dialog"`
- **Radix Alert Dialog:** <https://www.radix-ui.com/primitives/docs/components/alert-dialog>
- **APG Alert and Message Dialogs:** <https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/>
- **Page:** <https://ui.booleanpress.com/components/alert-dialog> · @booleanpress/ui 0.1.0

## Usage

Use an alert dialog when the person must confirm or cancel, and a [dialog](/components/dialog) when they do something richer. Clicking the backdrop does not close it: only Cancel, the action, or Escape does. `AlertDialogTitle` names it and `AlertDialogDescription` describes it; both are read when it opens. Focus starts on Cancel, the least destructive choice.

```tsx
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@booleanpress/ui/alert-dialog"
import { Button } from "@booleanpress/ui/button"

export function DeleteMailer() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Delete mailer</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Delete the Primary mailer?</AlertDialogTitle>
          <AlertDialogDescription>This cannot be undone.</AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Delete mailer</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
```

`AlertDialogAction` and `AlertDialogCancel` render a [button](/components/button) and take its `variant` and `size`: `destructive` for an action that removes something, `outline` for Cancel (the defaults). Both close the dialog; run the confirmed work in the action's `onClick`. `size="sm"` makes a narrow confirmation with the two buttons side by side. Control it with `open` and `onOpenChange` to open it from code.

If the action removes the row that held the trigger, focus has nowhere to return to. Pass `returnFocusTo` (a ref, or a function that returns an element) to `AlertDialogContent`.

## Examples

### Basic

A deletion confirmation with Cancel and a destructive action.

```tsx
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@booleanpress/ui/alert-dialog"
import { Button } from "@booleanpress/ui/button"

export default function AlertDialogBasic() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Delete mailer</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Delete the Primary mailer?</AlertDialogTitle>
          <AlertDialogDescription>
            Emails queued for this mailer stay in the log, but they are not sent. This cannot be undone.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Delete mailer</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
```

### Small

`size="sm"` is narrow, and the two buttons share the row.

```tsx
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@booleanpress/ui/alert-dialog"
import { Button } from "@booleanpress/ui/button"

export default function AlertDialogSmall() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Clear the log</Button>
      </AlertDialogTrigger>
      <AlertDialogContent size="sm">
        <AlertDialogHeader>
          <AlertDialogTitle>Clear the email log?</AlertDialogTitle>
          <AlertDialogDescription>All 1,284 entries are removed.</AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Clear</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
```

### With an icon

`AlertDialogMedia` puts an icon beside the title; mark the icon `aria-hidden`.

```tsx
import { TriangleAlertIcon } from "lucide-react"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@booleanpress/ui/alert-dialog"
import { Button } from "@booleanpress/ui/button"

export default function AlertDialogWithIcon() {
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <Button variant="outline">Rotate the API key</Button>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia>
            <TriangleAlertIcon aria-hidden="true" />
          </AlertDialogMedia>
          <AlertDialogTitle>Rotate the API key?</AlertDialogTitle>
          <AlertDialogDescription>
            The old key stops working at once. Update every site that uses it.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Keep the key</AlertDialogCancel>
          <AlertDialogAction>Rotate</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
```

### Return focus

Deleting a row removes its trigger, so `returnFocusTo` sends focus to the list's heading.

```tsx
import { useRef, useState } from "react"
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@booleanpress/ui/alert-dialog"
import { Button } from "@booleanpress/ui/button"

export default function AlertDialogReturnFocus() {
  const [keys, setKeys] = useState(["Production", "Staging", "Testing"])
  const heading = useRef<HTMLHeadingElement>(null)

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <h3 ref={heading} tabIndex={-1} className="text-sm font-medium outline-none">
        API keys ({keys.length})
      </h3>
      <ul className="flex flex-col gap-2">
        {keys.map((key) => (
          <li key={key} className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
            {key}
            <AlertDialog>
              <AlertDialogTrigger asChild>
                <Button variant="ghost" size="sm">
                  Delete<span className="sr-only"> {key} key</span>
                </Button>
              </AlertDialogTrigger>
              <AlertDialogContent returnFocusTo={heading}>
                <AlertDialogHeader>
                  <AlertDialogTitle>Delete the {key} key?</AlertDialogTitle>
                  <AlertDialogDescription>Sites that use it can no longer send email.</AlertDialogDescription>
                </AlertDialogHeader>
                <AlertDialogFooter>
                  <AlertDialogCancel>Cancel</AlertDialogCancel>
                  <AlertDialogAction variant="destructive" onClick={() => setKeys((list) => list.filter((k) => k !== key))}>
                    Delete
                  </AlertDialogAction>
                </AlertDialogFooter>
              </AlertDialogContent>
            </AlertDialog>
          </li>
        ))}
      </ul>
    </div>
  )
}
```

## Accessibility

**Semantics.** A `div` with `role="alertdialog"` and `aria-modal="true"`, named by `AlertDialogTitle` and described by `AlertDialogDescription`. The rest of the page is hidden from assistive technology while it is open.

**Labels.** Every alert dialog needs a title and a description that says what will happen. Name the buttons by what they do ("Delete mailer"), not "OK". Several triggers of the same kind need names that differ, for example a visually hidden row name.

**Focus.** Focus moves to Cancel when it opens, Tab and Shift+Tab stay inside, and focus returns to the trigger when it closes. Without a trigger it returns to the element that had focus when the dialog opened, or to `returnFocusTo` when that element is gone.

**Known limits.**

- It has no × button and ignores clicks on the backdrop, on purpose. Always provide Cancel.
- One dialog at a time: an alert dialog does not open a second dialog.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Escape | Closes it without running the action. |
| Tab | Moves to the next button inside the dialog, wrapping from the last to the first. |
| Shift + Tab | Moves to the previous button, wrapping from the first to the last. |
| Enter + Space | Presses the focused button. |

## API

### AlertDialog

Renders Radix AlertDialog.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  | Whether it starts open, when it controls itself. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether it is open, when you control it. Pair it with `onOpenChange`. |

### AlertDialogAction

Renders Radix AlertDialog.Action and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### AlertDialogCancel

Renders Radix AlertDialog.Cancel and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### AlertDialogContent

Renders Radix AlertDialog.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `deferPointerDownOutside` | `boolean` |  | When `true`, a `'pointerdown'` event outside of the layered element will wait for the interaction's click event before dispatching, allowing third-party code to stop propagation of later events and cancel dismissal. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
| `onOpenAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on open. Can be prevented. |
| `returnFocusTo` | `ReturnFocusTarget` |  | Where focus goes on close when the element that opened the overlay no longer exists — for example after the confirmed action deleted the row it sat in. |
| `size` | `"default" \| "sm"` | `default` | `sm` is a narrow, centred confirmation. |

### AlertDialogDescription

Renders Radix AlertDialog.Description and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### AlertDialogFooter

Renders a `div` and passes it every other prop.

### AlertDialogHeader

Renders a `div` and passes it every other prop.

### AlertDialogMedia

Renders a `div` and passes it every other prop.

### AlertDialogOverlay

Renders Radix AlertDialog.Overlay and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |

### AlertDialogPortal

Renders Radix AlertDialog.Portal and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `container` | `Element \| DocumentFragment \| null` |  | Specify a container element to portal the content into. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |

### AlertDialogTitle

Renders Radix AlertDialog.Title and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### AlertDialogTrigger

Renders Radix AlertDialog.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="alert-dialog"` (AlertDialog), `data-slot="alert-dialog-action"` (AlertDialogAction), `data-slot="alert-dialog-cancel"` (AlertDialogCancel), `data-slot="alert-dialog-content"` (AlertDialogContent), `data-slot="alert-dialog-description"` (AlertDialogDescription), `data-slot="alert-dialog-footer"` (AlertDialogFooter), `data-slot="alert-dialog-header"` (AlertDialogHeader), `data-slot="alert-dialog-media"` (AlertDialogMedia), `data-slot="alert-dialog-overlay"` (AlertDialogOverlay), `data-slot="alert-dialog-portal"` (AlertDialogPortal), `data-slot="alert-dialog-title"` (AlertDialogTitle), `data-slot="alert-dialog-trigger"` (AlertDialogTrigger), and `data-size`.

## Theming

The panel is a floating surface, like menus and popovers: `--popover` and `--popover-foreground`. Its height stops below the WordPress admin bar (`--wp-admin--admin-bar--height`).

| Token | Used for |
| --- | --- |
| `--background` | background |
| `--muted` | background |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
