Alert Dialog
An alert dialog stops everything to ask one question. Use it before something that can't be undone, when a wrong click would cost the person real work.
import {
AlertDialog,
AlertDialogClose,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export default function AlertDialogDemo() {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="destructive" />}>Delete account</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete your account?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes your account and all of its data. This can't be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogClose render={<Button variant="secondary" />}>Cancel</AlertDialogClose>
<AlertDialogClose render={<Button variant="destructive" />}>Delete account</AlertDialogClose>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}The code
The component is a single file, read from the repository when the site is built. Press Show code to see all of it.
import type { ComponentProps } from "react";
import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog";
import { cx } from "@/lib/cx";
import { backdropStyles, popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";
export const AlertDialog = BaseAlertDialog.Root;
// Renders a button by default. To open it from your own element, such as a Button, pass it as
// render: <AlertDialogTrigger render={<Button />}>.
export const AlertDialogTrigger = BaseAlertDialog.Trigger;
// Like a Dialog, but for a question that needs an answer: clicking the backdrop does not close
// it, and there is no close button in the corner. The person has to choose one of the actions.
export function AlertDialogContent({
className,
...props
}: WithClass<ComponentProps<typeof BaseAlertDialog.Popup>>) {
return (
<BaseAlertDialog.Portal>
<BaseAlertDialog.Backdrop className={backdropStyles} />
<BaseAlertDialog.Popup
className={cx(
popupSurface,
popupMotion,
"fixed top-1/2 left-1/2 z-50 flex w-[28rem] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 flex-col gap-4 p-6",
className,
)}
{...props}
/>
</BaseAlertDialog.Portal>
);
}
export function AlertDialogHeader({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("flex flex-col gap-1.5", className)} {...props} />;
}
export function AlertDialogTitle({ className, ...props }: WithClass<ComponentProps<typeof BaseAlertDialog.Title>>) {
return <BaseAlertDialog.Title className={cx("text-xl leading-tight font-semibold text-text", className)} {...props} />;
}
export function AlertDialogDescription({
className,
...props
}: WithClass<ComponentProps<typeof BaseAlertDialog.Description>>) {
return <BaseAlertDialog.Description className={cx("text-base text-text/70", className)} {...props} />;
}
export function AlertDialogFooter({ className, ...props }: ComponentProps<"div">) {
return <div className={cx("flex flex-col gap-2 sm:flex-row sm:justify-end", className)} {...props} />;
}
// Each action closes the dialog. Pass a Button as render to style it:
// <AlertDialogClose render={<Button variant="destructive" />}>Delete</AlertDialogClose>.
export const AlertDialogClose = BaseAlertDialog.Close;Props
| Prop | Type | Default | What it does |
|---|---|---|---|
defaultOpen | boolean | false | Starts the dialog open, when it manages its own state. |
open | boolean | — | Whether it is open, when you control it yourself. |
onOpenChange | function | — | Called with the new open state. |
render | element | — | On AlertDialogTrigger and AlertDialogClose. Uses your own element, such as a Button, instead of a plain button. |
Every other prop goes to the Base UI AlertDialog underneath. Give it an AlertDialogTitle and an AlertDialogDescription.
The thinking
Most dialogs can be waved away. This one can't, on purpose: the person has to choose an answer.
- Built on Base UI: Base UI marks it as an alert dialog, holds focus inside, restores it on close and locks the page behind.
- Different from a Dialog: Clicking the backdrop does nothing and there is no corner button. Escape, Cancel or the main action are the ways out.
- Say what will happen: Title it as a question and name the consequence in the description. The buttons repeat the verb, like Delete account, rather than OK.
Interface design
- Same window: The same surface, backdrop and size as a Dialog, so the two feel like one family.
- Destructive action: When the answer can't be undone, the main button is the destructive one, in red. Cancel is the secondary button next to it.
- No corner button: The only choices are in the footer, where the eyes are already reading.
Interaction
- Open: Click the trigger, or press Enter or Space when it has focus.
- Focus: Focus moves into the dialog and Tab stays inside it. When it closes, focus goes back to the trigger.
- Close: Escape, Cancel, or the main action. A click on the backdrop does not close it.
- Motion: A 150ms fade, with a small scale on the window. With reduced motion turned on it appears at once.
Empathy
- Announced as an alert: Screen readers announce it right away, with its title and description.
- Easy to back out: Escape and Cancel always work, so a dialog opened by mistake costs nothing.
- Use it rarely: If every action asks for confirmation, people click through without reading. Keep it for what can't be undone, and offer Undo for the rest.