AlertDialog
A modal that interrupts with something important and waits for an answer.
Basic
A press outside does nothing; Escape or Cancel closes it. Focus starts on Cancel. onOpenChangeComplete fires when the enter and exit animations end.
Code
<AlertDialog title="Are you absolutely sure?"
description="This action cannot be undone. This will permanently delete your account and remove your data from our servers.">
<Button slot="trigger">Show dialog</Button>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onclick={() => remove(account)}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Usage
import { AlertDialog, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "@elements/ui/alert-dialog";
<AlertDialog title="Delete project?" description="This permanently deletes the project and its files.">
<Button slot="trigger" variant="danger">Delete project</Button>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" onclick={() => remove(project)}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
<!-- controlled: open is two-way -->
<AlertDialog open={state.confirming} title="Discard changes?">...</AlertDialog>Examples
Small
size="sm": narrow and centered, with the two actions side by side.
Code
<AlertDialog size="sm" title="Allow accessory to connect?"
description="Do you want to allow the USB accessory to connect to this device?">
<Button slot="trigger">Show small dialog</Button>
<AlertDialogFooter>
<AlertDialogCancel>Don't allow</AlertDialogCancel>
<AlertDialogAction>Allow</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Media
AlertDialogMedia in the media slot: an icon beside the title from 40rem up, above it on phones.
Code
<AlertDialog title="Share this project?"
description="Anyone with the link will be able to view and edit this project.">
<Button slot="trigger">Share project</Button>
<AlertDialogMedia slot="media"><Icon name="external-link" /></AlertDialogMedia>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Share</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Small with media
The sm size keeps the media centered above the title.
Code
<AlertDialog size="sm" title="Turn on notifications?" description="...">
<Button slot="trigger">Notifications</Button>
<AlertDialogMedia slot="media"><Icon name="bell" /></AlertDialogMedia>
...
</AlertDialog>Destructive
A danger Action and danger media for an action that cannot be undone.
Code
<AlertDialog size="sm" title="Delete chat?" description="This will permanently delete this chat conversation.">
<Button slot="trigger" variant="danger">Delete chat</Button>
<AlertDialogMedia slot="media" variant="danger"><Icon name="trash" /></AlertDialogMedia>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Close after async work
The Action calls e.preventDefault() to stay open, shows loading at the same size, then the page closes the dialog. Escape is ignored while it runs.
Code
<AlertDialog open={state.open} title="Delete 12 files?"
onEscapeKeyDown={(e) => guard(state, e)}>
<Button slot="trigger" variant="danger">Delete files</Button>
<AlertDialogFooter>
<AlertDialogCancel disabled={state.deleting}>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" loading={state.deleting}
onclick={(e) => remove(state, e)}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
async function remove(state, e) {
e.preventDefault(); // stay open while the work runs
state.deleting = true;
await rpc.deleteFiles();
state.deleting = false;
state.open = false;
}Title and description parts
AlertDialogHeader, AlertDialogTitle and AlertDialogDescription take rich content and are wired to aria-labelledby and aria-describedby.
Code
<AlertDialog>
<Button slot="trigger">Leave page</Button>
<AlertDialogHeader>
<AlertDialogTitle>Leave <em>Untitled draft</em>?</AlertDialogTitle>
<AlertDialogDescription>Your changes have not been saved.</AlertDialogDescription>
</AlertDialogHeader>
...
</AlertDialog>From a dialog
An alert dialog opened inside a dialog. Escape closes only the alert, and focus returns to the button that opened it.
Code
<Dialog title="Edit document">
<Button slot="trigger">Edit document</Button>
<textarea>...</textarea>
<DialogFooter>
<AlertDialog title="Discard changes?" description="Your edits will be lost.">
<Button slot="trigger" variant="ghost">Discard</Button>
...
</AlertDialog>
</DialogFooter>
</Dialog>No visible title
With no title, the label attribute names the dialog with aria-label, and the description is still the alert message wired to aria-describedby.
Code
<AlertDialog label="Session expiring" description="Your session expires in 2 minutes. Stay signed in?">
<Button slot="trigger">Session warning</Button>
...
</AlertDialog>Focus on open and close
initialFocus names the element focused on open in place of Cancel; use it when the action is safe. returnFocus={false} leaves focus where it lands on close. onEscapeKeyDown runs before Escape closes, and preventDefault would keep it open.
Code
<AlertDialog title="Publish this post?" description="..."
initialFocus="[data-alert-dialog-action]" returnFocus={false}
onEscapeKeyDown={(e) => log("escape")}>
<Button slot="trigger">Publish</Button>
<AlertDialogFooter>
<AlertDialogCancel>Not yet</AlertDialogCancel>
<AlertDialogAction>Publish</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Confirm before discarding
The WAI-ARIA example: Discard asks first. No closes without changes, Yes empties the notes. Focus starts on No, the least destructive answer, and finalFocus sends it back to the notes field on close. With the notes empty Discard is aria-disabled: it stays in the Tab order but does nothing.
Code
<textarea aria-label="Notes" value={state.notes} oninput={(e) => state.notes = e.target.value}></textarea>
<AlertDialog title="Confirmation" description="Are you sure you want to discard all of your notes?"
finalFocus="[data-notes]">
<!-- aria-disabled: Discard stays in the Tab order once the notes are empty -->
<Button slot="trigger" disabled={!state.notes} focusableWhenDisabled>Discard</Button>
<AlertDialogFooter>
<AlertDialogCancel>No</AlertDialogCancel>
<AlertDialogAction variant="danger" onclick={() => state.notes = ""}>Yes</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Detached triggers
Buttons anywhere on the page open one controlled alert dialog and pass what it is about through page state. Focus returns to the button used. Opening it from page state is the caller's change, so onOpenChange reports only the dialog's own close.
Code
<Button onclick={() => discard(state, "Q3 plan")}>Discard Q3 plan</Button>
<Button onclick={() => discard(state, "Hiring memo")}>Discard Hiring memo</Button>
<AlertDialog open={state.open} title={`Discard ${state.doc}?`}
description="The draft is deleted for everyone."
onOpenChange={(open, reason) => log(reason)}>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger">Discard</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
function discard(state, doc) {
state.doc = doc; // the payload
state.open = true; // focus returns to the button used
}Open from a menu
A menu item opens a controlled alert dialog. The item is gone by the time it closes, so focus goes back to the menu's button.
Code
<DropdownMenu>
<Button slot="trigger">Playlist</Button>
<DropdownMenuItem onSelect={() => state.deleting = true}>Delete playlist...</DropdownMenuItem>
</DropdownMenu>
<AlertDialog open={state.deleting} title="Delete playlist?" description="...">
...
</AlertDialog>Focus events
onOpenAutoFocus and onCloseAutoFocus run before focus moves; preventDefault() leaves focus to the handler. Here Archive takes focus on open, and focus stays off the trigger on close.
Code
<AlertDialog title="Archive 3 threads?"
onOpenAutoFocus={(e) => { e.preventDefault(); archive.focus(); }}
onCloseAutoFocus={(e) => e.preventDefault()}>
...
</AlertDialog>Focus by interaction type
initialFocus and finalFocus can be functions of how the alert opened or closed: keyboard, mouse, touch or pen. Returning true keeps the default, Cancel.
Code
<AlertDialog title="Discard the import?" description="Name the batch to keep it as a draft instead."
initialFocus={(type) => type === "keyboard" ? "[data-batch]" : true}
finalFocus={(type) => type === "keyboard" ? true : "[data-after]"}>
<Button slot="trigger">Discard import</Button>
<input aria-label="Batch name" data-batch>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger">Discard</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Open on load
open={true} without a binding: the alert is open when the page loads, then owns its state. Cancel closes it and the trigger opens it again.
Code
<AlertDialog open={true} title="Your trial ends today" description="Add a payment method to keep your projects running.">
<Button slot="trigger">Trial notice</Button>
...
</AlertDialog>Confirm with a form
A form inside the alert: the Action is its submit button, so a click or Enter in the field submits it.
Code
<AlertDialog title="Delete repository?" description="Type the name to confirm.">
<Button slot="trigger" variant="danger">Delete repository</Button>
<form onsubmit={(e) => remove(e)}>
<input name="confirm" aria-label="Repository name">
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" type="submit">Delete</AlertDialogAction>
</AlertDialogFooter>
</form>
</AlertDialog>Escape reaches the page
onEscapeKeyDown can call allowPropagation(): Escape closes the alert and still reaches the page's own key handlers.
Code
<AlertDialog title="Stop the upload?" description="Files sent so far are kept." onEscapeKeyDown={(e) => e.allowPropagation()}>
<Button slot="trigger">Stop upload</Button>
<AlertDialogFooter>
<AlertDialogCancel>Keep uploading</AlertDialogCancel>
<AlertDialogAction>Stop</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>Right to left
Under dir="rtl" the header aligns to the start edge and the actions mirror.
Code
<div dir="rtl">
<AlertDialog title="هل أنت متأكد تماما؟" description="لا يمكن التراجع عن هذا الإجراء.">
<Button slot="trigger">إظهار الحوار</Button>
...
</AlertDialog>
</div>