AlertDialog for deletes
Stray clicks can’t close it, and focus opens on Cancel.
From shadcn (Vercel plugin): Destructive flows and Anti-patterns.
One delete button, two dialogs
Open each, then click outside, press Escape, or press Enter.
DialogBad
<Dialog>deleted 0 · no answer 0AlertDialogGood
acme-web
Active<AlertDialog>deleted 0 · no answer 0<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete “acme-web”?</AlertDialogTitle>
<AlertDialogDescription>
Its 3 deployments and 2 domains go with it. This can’t be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={deleteProject}>Delete project</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
The hasty keyboard run
Press Run, then move AlertDialog focus to Delete.
DialogBad
Enter
| Project | Status | Actions |
|---|
<Dialog>deleted 0 of 4AlertDialogGood
Enter
| Project | Status | Actions |
|---|
focus: AlertDialogCanceldeleted 0 of 4const [target, setTarget] = useState<Project | null>(null)
<DropdownMenuItem variant="destructive" onSelect={() => setTarget(project)}>
Delete
</DropdownMenuItem>
<AlertDialog open={target !== null} onOpenChange={(open) => !open && setTarget(null)}>
<AlertDialogContent>
<AlertDialogHeader>…</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> {/* focused on open */}
<AlertDialogAction onClick={() => remove(target.id)}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Why it matters, pros and cons
Why it matters
Dialog closes on a stray click; Enter hits whatever has focus. AlertDialog waits.
Pros
- A double Enter lands on Cancel.
- A stray click can’t dismiss it.
- Screen readers announce
alertdialog.
Cons
- Reversible actions want an undo toast, not a modal.
- Confirming everything teaches people to click through.
- Costly deletes also need type-the-name confirmation.