# Alert Dialog

Ask for a deliberate decision before continuing.

Delete project confirmation

[Complete TSX file](/examples/alert-dialog/default.tsx)

```tsx
"use client";

import {
  AlertDialog,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogPopup,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/siteplane/components/alert-dialog";
import { Button } from "@/siteplane/components/button";

export default function Example() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline-destructive" />}>
        Delete project
      </AlertDialogTrigger>
      <AlertDialogPopup>
        <AlertDialogHeader>
          <AlertDialogTitle>Delete project?</AlertDialogTitle>
          <AlertDialogDescription>
            This permanently deletes Website Redesign, including its tasks and
            files. This action can&apos;t be undone.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogClose render={<Button variant="ghost" />}>
            Cancel
          </AlertDialogClose>
          <AlertDialogClose render={<Button variant="destructive" />}>
            Delete project
          </AlertDialogClose>
        </AlertDialogFooter>
      </AlertDialogPopup>
    </AlertDialog>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/alert-dialog
```

With npm: `npx shadcn@4.18.0 add @siteplane/alert-dialog`. [Registry JSON](/registry/alert-dialog.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/alert @siteplane/button @siteplane/dialog @siteplane/form @siteplane/input @siteplane/motion @siteplane/textarea
```

Field is installed with Input.

## Usage

```tsx
import {
  AlertDialog,
  AlertDialogTrigger,
  AlertDialogPopup,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogClose,
} from "@/siteplane/components/alert-dialog";
import { Button } from "@/siteplane/components/button";

<AlertDialog>
  <AlertDialogTrigger render={<Button variant="outline-destructive" />}>
    Delete project
  </AlertDialogTrigger>
  <AlertDialogPopup>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete project?</AlertDialogTitle>
      <AlertDialogDescription>
        This action can’t be undone.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogClose render={<Button variant="ghost" />}>
        Cancel
      </AlertDialogClose>
      <AlertDialogClose render={<Button variant="destructive" />}>
        Delete
      </AlertDialogClose>
    </AlertDialogFooter>
  </AlertDialogPopup>
</AlertDialog>;
```

## Composition

```text
AlertDialog
├── AlertDialogTrigger
└── AlertDialogPopup / AlertDialogContent
    ├── AlertDialogHeader
    │   ├── AlertDialogTitle
    │   └── AlertDialogDescription
    └── AlertDialogFooter
        └── AlertDialogClose
```

## Confirm a request

When the confirmed action runs a request, control the root with `open` and `onOpenChange`. The destructive button shows `loading` while the request runs, `onOpenChange` ignores dismissal until it settles, and the dialog closes only after the request succeeds. If the request fails, the dialog stays open and an error [Alert](/docs/components/alert/) opens under the description in a [MotionReveal](/docs/components/motion/#show-and-hide-a-block), so screen readers announce it. The message closes when the person tries again, and it is gone the next time the dialog opens. The request in this example fails on the first try, so you can see the loading state, the error and the success.

Delete account

[Complete TSX file](/examples/alert-dialog/delete-account.tsx)

```tsx
"use client";

import { CircleAlertIcon } from "lucide-react";
import {
  Alert,
  AlertDescription,
  AlertTitle,
} from "@/siteplane/components/alert";
import {
  AlertDialog,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogPopup,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/siteplane/components/alert-dialog";
import { Button } from "@/siteplane/components/button";
import { MotionReveal } from "@/siteplane/components/motion";
import { useRef, useState } from "react";

// Stands in for the request that deletes the account. Here it fails on the
// first try, so the example shows the error and a retry.
function deleteAccount(fail: boolean): Promise<void> {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      if (fail) reject(new Error("The request failed."));
      else resolve();
    }, 1400);
  });
}

export default function Example() {
  const [open, setOpen] = useState(false);
  const [pending, setPending] = useState(false);
  const [failed, setFailed] = useState(false);
  const attempts = useRef(0);

  return (
    <AlertDialog
      open={open}
      onOpenChange={(nextOpen) => {
        // While the request runs, the confirmation stays open.
        if (!pending) setOpen(nextOpen);
      }}
      onOpenChangeComplete={(isOpen) => {
        // A closed confirmation starts over the next time it opens.
        if (!isOpen) {
          setFailed(false);
          attempts.current = 0;
        }
      }}
    >
      <AlertDialogTrigger render={<Button variant="ghost-destructive" />}>
        Delete account
      </AlertDialogTrigger>
      <AlertDialogPopup>
        <AlertDialogHeader>
          <AlertDialogTitle>Delete your account?</AlertDialogTitle>
          <div>
            <AlertDialogDescription>
              This permanently deletes your account and removes your data from
              our servers. This action can&apos;t be undone.
            </AlertDialogDescription>
            <MotionReveal open={failed}>
              <Alert className="mt-3" variant="error">
                <CircleAlertIcon aria-hidden="true" />
                <AlertTitle>Your account wasn&apos;t deleted</AlertTitle>
                <AlertDescription>
                  The request failed. Try again.
                </AlertDescription>
              </Alert>
            </MotionReveal>
          </div>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogClose
            disabled={pending}
            render={<Button variant="ghost" />}
          >
            Cancel
          </AlertDialogClose>
          <Button
            loading={pending}
            variant="destructive"
            onClick={async () => {
              attempts.current += 1;
              setFailed(false);
              setPending(true);
              try {
                await deleteAccount(attempts.current === 1);
                setOpen(false);
              } catch {
                setFailed(true);
              } finally {
                setPending(false);
              }
            }}
          >
            Delete account
          </Button>
        </AlertDialogFooter>
      </AlertDialogPopup>
    </AlertDialog>
  );
}
```

## Close confirmation

Control the alert dialog with `open` and `onOpenChange` to confirm before a dialog with unsaved input closes. While the comment has text, the parent dialog’s `onOpenChange` opens the confirmation instead of closing.

Discard unsaved comment confirmation

[Complete TSX file](/examples/alert-dialog/controlled.tsx)

```tsx
"use client";

import {
  AlertDialog,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogPopup,
  AlertDialogTitle,
} from "@/siteplane/components/alert-dialog";
import { Button } from "@/siteplane/components/button";
import {
  Dialog,
  DialogClose,
  DialogPopup,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogPanel,
  DialogTitle,
  DialogTrigger,
} from "@/siteplane/components/dialog";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Textarea } from "@/siteplane/components/textarea";
import { useState } from "react";

export default function Example() {
  const [dialogOpen, setDialogOpen] = useState(false);
  const [confirmOpen, setConfirmOpen] = useState(false);
  const [comment, setComment] = useState("");

  return (
    <Dialog
      open={dialogOpen}
      onOpenChange={(open) => {
        if (!open && comment) {
          setConfirmOpen(true);
        } else {
          setDialogOpen(open);
        }
      }}
    >
      <DialogTrigger render={<Button variant="outline" />}>
        Add comment
      </DialogTrigger>
      <DialogPopup>
        <Form
          className="contents"
          onSubmit={(event) => {
            event.preventDefault();
            setComment("");
            setDialogOpen(false);
          }}
        >
          <DialogHeader>
            <DialogTitle>New comment</DialogTitle>
            <DialogDescription>
              Everyone on this project can read your comment.
            </DialogDescription>
          </DialogHeader>
          <DialogPanel>
            <Field>
              <FieldLabel>Comment</FieldLabel>
              <Textarea
                value={comment}
                onChange={(event) => setComment(event.target.value)}
                required
              />
              <FieldError match="valueMissing">Enter a comment.</FieldError>
            </Field>
          </DialogPanel>
          <DialogFooter>
            <DialogClose render={<Button variant="ghost" />}>
              Cancel
            </DialogClose>
            <Button type="submit">Post comment</Button>
          </DialogFooter>
        </Form>
      </DialogPopup>
      <AlertDialog open={confirmOpen} onOpenChange={setConfirmOpen}>
        <AlertDialogPopup>
          <AlertDialogHeader>
            <AlertDialogTitle>Discard comment?</AlertDialogTitle>
            <AlertDialogDescription>
              Your comment hasn&apos;t been posted and will be lost.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogClose render={<Button variant="ghost" />}>
              Keep editing
            </AlertDialogClose>
            <Button
              variant="destructive"
              onClick={() => {
                setConfirmOpen(false);
                setComment("");
                setDialogOpen(false);
              }}
            >
              Discard
            </Button>
          </AlertDialogFooter>
        </AlertDialogPopup>
      </AlertDialog>
    </Dialog>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| AlertDialogPopup.size | "xs" \| "sm" \| "md" \| "lg" | sm | Popup width from 640 px up: 400, 440, 600 or 880 px. Below 640 px the popup is a full-width sheet at the bottom edge. |
| AlertDialogPopup.portalProps | AlertDialogPrimitive.Portal.Props | unset | Portal container/mount options. |
| open / defaultOpen | boolean | false | Controlled or initial open state. |
| onOpenChange | (open, eventDetails) => void | unset | Opening/dismissal callback. |
| onOpenChangeComplete | (open) => void | unset | After open/close motion completes. |
`AlertDialog` and `AlertDialogPortal` are direct foundation exports. Trigger and
Close wrap the foundation action parts. Popup includes Portal, Backdrop,
Viewport and portal scope automatically; do not wrap it in another portal.
Header/Footer are div layouts that accept `render`, as in Dialog. Title and
Description supply accessible dialog naming. `AlertDialogContent` and
`AlertDialogOverlay` are the shadcn names of `AlertDialogPopup` and
`AlertDialogBackdrop`; both keep working. `AlertDialogCreateHandle` creates
detached-trigger handles; `AlertDialogPrimitive` exposes the namespace.

The `size` scale is the one [Dialog](/docs/components/dialog/#size) and the Modal Block use. A `max-w-*` class in the popup's `className` still sets its own width, also below 640 px.

Backdrop/Viewport and the styled parts accept string className; all remaining props and refs follow [Base UI Alert Dialog](https://base-ui.com/react/components/alert-dialog). Popup initialFocus/finalFocus control focus; default behavior belongs to the foundation. There is no built-in confirm callback or async loading API; [Confirm a request](#confirm-a-request) shows how to compose one.

`AlertDialogBackdrop` and `AlertDialogViewport` wrap their matching Base UI parts with the shared overlay backdrop and centered viewport styles. They accept the foundation props and string className. AlertDialogPopup already includes both; use them separately only when composing from the primitive popup.

[Read the installed source](/source/alert-dialog).

## Usage notes

Focus is contained while open and restored on close. Provide a clear cancel action, and use Alert Dialog only for a decision the user must make before going on. For asynchronous confirmation, keep the root controlled and close only after success, as in [Confirm a request](#confirm-a-request); show an error message when the request fails.
