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&apos;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

PropTypeDefaultWhat it does
defaultOpenbooleanfalseStarts the dialog open, when it manages its own state.
openboolean—Whether it is open, when you control it yourself.
onOpenChangefunction—Called with the new open state.
renderelement—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.