Siteplane UI

Alert Dialog

Ask for a deliberate decision before continuing.

Markdown
"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

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

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

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

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 opens under the description in a MotionReveal, 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.

"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.

"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

Component properties
PropTypeDefaultDescription
AlertDialogPopup.size"xs" | "sm" | "md" | "lg"smPopup 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.portalPropsAlertDialogPrimitive.Portal.PropsunsetPortal container/mount options.
open / defaultOpenbooleanfalseControlled or initial open state.
onOpenChange(open, eventDetails) => voidunsetOpening/dismissal callback.
onOpenChangeComplete(open) => voidunsetAfter 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 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. 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 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.

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; show an error message when the request fails.