---
name: alert-dialog
type: core
domain: overlays
requires: [loke-ui, dialog]
description: >
  Confirmation dialogs for destructive actions. AlertDialog, AlertDialogTrigger,
  AlertDialogPortal, AlertDialogOverlay, AlertDialogContent, AlertDialogTitle,
  AlertDialogDescription, AlertDialogAction, AlertDialogCancel. Always modal,
  always prevents outside dismiss, auto-focuses cancel button.
---

# Alert Dialog

This skill builds on dialog. Read it first: [dialog](../dialog/SKILL.md).

`AlertDialog` is a strict subset of `Dialog` for destructive confirmations. It hardcodes `modal={true}`, prevents `onPointerDownOutside` and `onInteractOutside` dismissal, and auto-focuses `AlertDialogCancel` on open. Use it whenever the action cannot be undone.

## Setup

Minimum working confirmation dialog.

```tsx
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@loke/ui/alert-dialog";

function DeleteButton() {
  return (
    <AlertDialog>
      <AlertDialogTrigger>Delete item</AlertDialogTrigger>
      <AlertDialogPortal>
        <AlertDialogOverlay />
        <AlertDialogContent>
          <AlertDialogTitle>Delete item?</AlertDialogTitle>
          <AlertDialogDescription>
            This action cannot be undone. The item will be permanently removed.
          </AlertDialogDescription>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Delete</AlertDialogAction>
        </AlertDialogContent>
      </AlertDialogPortal>
    </AlertDialog>
  );
}
```

On open, focus moves to `AlertDialogCancel` automatically. Clicking outside does nothing. Escape closes the dialog and returns focus to the trigger.

## Core Patterns

### Controlled open state

```tsx
import { useState } from "react";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogTitle,
} from "@loke/ui/alert-dialog";

function ConfirmDiscard({ onDiscard }: { onDiscard: () => void }) {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Discard changes
      </button>
      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialogPortal>
          <AlertDialogOverlay />
          <AlertDialogContent>
            <AlertDialogTitle>Discard changes?</AlertDialogTitle>
            <AlertDialogDescription>
              Unsaved changes will be lost.
            </AlertDialogDescription>
            <AlertDialogCancel>Keep editing</AlertDialogCancel>
            <AlertDialogAction onClick={onDiscard}>Discard</AlertDialogAction>
          </AlertDialogContent>
        </AlertDialogPortal>
      </AlertDialog>
    </>
  );
}
```

`AlertDialogTrigger` is optional. You can control open state directly and drive the dialog from any button.

### Custom action handling

`AlertDialogAction` closes the dialog automatically (it wraps `DialogClose`). To run async work before closing, prevent default and manage state yourself.

```tsx
import { useState } from "react";
import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@loke/ui/alert-dialog";

function AsyncDeleteButton({ onDelete }: { onDelete: () => Promise<void> }) {
  const [open, setOpen] = useState(false);
  const [loading, setLoading] = useState(false);

  async function handleDelete(event: React.MouseEvent) {
    event.preventDefault(); // prevent auto-close
    setLoading(true);
    await onDelete();
    setLoading(false);
    setOpen(false);
  }

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialogTrigger>Delete</AlertDialogTrigger>
      <AlertDialogPortal>
        <AlertDialogOverlay />
        <AlertDialogContent>
          <AlertDialogTitle>Delete?</AlertDialogTitle>
          <AlertDialogDescription>This cannot be undone.</AlertDialogDescription>
          <AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
          <AlertDialogAction onClick={handleDelete} disabled={loading}>
            {loading ? "Deleting..." : "Delete"}
          </AlertDialogAction>
        </AlertDialogContent>
      </AlertDialogPortal>
    </AlertDialog>
  );
}
```

## Common Mistakes

### Missing AlertDialogDescription

`AlertDialogContent` warns in dev if `aria-describedby` resolves to nothing. Screen readers announce the description when the dialog opens, giving users the context to confirm or cancel.

```tsx
// Wrong — no description
<AlertDialogContent>
  <AlertDialogTitle>Delete item?</AlertDialogTitle>
  <AlertDialogCancel>Cancel</AlertDialogCancel>
  <AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogContent>

// Correct
<AlertDialogContent>
  <AlertDialogTitle>Delete item?</AlertDialogTitle>
  <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
  <AlertDialogCancel>Cancel</AlertDialogCancel>
  <AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogContent>
```

Source: `src/components/alert-dialog/alert-dialog.tsx` — `DescriptionWarning` dev check.

### Missing AlertDialogCancel — nothing auto-focuses

`AlertDialogContent` calls `cancelRef.current?.focus()` inside `onOpenAutoFocus`. Without `AlertDialogCancel`, `cancelRef` is null and focus falls to the first tabbable element, which may be the destructive `AlertDialogAction` button.

```tsx
// Wrong — focus lands on the destructive button
<AlertDialogContent>
  <AlertDialogTitle>Delete?</AlertDialogTitle>
  <AlertDialogDescription>Cannot be undone.</AlertDialogDescription>
  <AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogContent>

// Correct — cancel gets focus, safer default
<AlertDialogContent>
  <AlertDialogTitle>Delete?</AlertDialogTitle>
  <AlertDialogDescription>Cannot be undone.</AlertDialogDescription>
  <AlertDialogCancel>Cancel</AlertDialogCancel>
  <AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogContent>
```

Source: `src/components/alert-dialog/alert-dialog.tsx` — `onOpenAutoFocus` focuses `cancelRef`.

### Setting modal={false} on AlertDialog

`AlertDialog` internally passes `modal={true}` to the underlying `Dialog` unconditionally. The `modal` prop is omitted from `AlertDialogProps`. Passing `modal={false}` has no effect.

```tsx
// This does nothing — AlertDialog is always modal
<AlertDialog modal={false}>
```

Source: `src/components/alert-dialog/alert-dialog.tsx` — `modal={true}` hardcoded in root component.

## Cross-references

- See also: [dialog](../dialog/SKILL.md) — base dialog pattern; read before this skill
- See also: [choosing-the-right-component](../choosing-the-right-component/SKILL.md) — Dialog vs AlertDialog decision
