@loke/ui
Version:
215 lines (177 loc) • 6.97 kB
Markdown
---
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