Use Alert Dialog before a consequential operation. Use Dialog for ordinary editing. Explain the actual consequence instead of asking only “Are you sure?”.
Live preview
"use client";
import { useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
export default function AlertDialogDemo() {
const [message, setMessage] = useState("");
return (
<div className="grid justify-items-center gap-3">
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}>
Archive release candidate
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Archive this release candidate?</AlertDialogTitle>
<AlertDialogDescription>
It will leave the active review queue. You can restore it later from the archive.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep active</AlertDialogCancel>
<AlertDialogAction
onClick={() => setMessage("Release candidate archived in this local preview.")}
>
Archive candidate
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<output className="block min-h-5 text-sm" aria-live="polite">
{message}
</output>
</div>
);
}
#Installation
Complete the base setup first. The CLI includes dependencies. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/alert-dialog.jsonnpm install @base-ui/react@^1.8.0Also install Button, then create components/ui/alert-dialog.tsx:
"use client";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import type * as React from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type AlertDialogContentProps = AlertDialogPrimitive.Popup.Props & {
size?: "default" | "sm";
};
type AlertDialogButtonProps = AlertDialogPrimitive.Close.Props &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">;
function AlertDialog(props: AlertDialogPrimitive.Root.Props) {
return <AlertDialogPrimitive.Root {...props} />;
}
function AlertDialogTrigger(props: AlertDialogPrimitive.Trigger.Props) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
}
function AlertDialogPortal(props: AlertDialogPrimitive.Portal.Props) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
}
function AlertDialogOverlay({ className, ...props }: AlertDialogPrimitive.Backdrop.Props) {
return (
<AlertDialogPrimitive.Backdrop
data-slot="alert-dialog-overlay"
{...props}
className={(state) =>
cn(
"fixed inset-0 isolate z-50 bg-overlay duration-200 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function AlertDialogContent({
className,
children,
size = "default",
...props
}: AlertDialogContentProps) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
data-slot="alert-dialog-content"
data-size={size}
{...props}
className={(state) =>
cn(
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100dvh-2rem)] w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto rounded-base border-2 border-border bg-background p-6 text-foreground shadow-shadow duration-200 outline-none data-[size=default]:sm:max-w-lg data-[size=sm]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
typeof className === "function" ? className(state) : className,
)
}
>
{children}
</AlertDialogPrimitive.Popup>
</AlertDialogPortal>
);
}
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
);
}
function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn("flex flex-col-reverse gap-3 sm:flex-row sm:justify-end", className)}
{...props}
/>
);
}
function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-media"
className={cn(
"mb-2 inline-flex size-10 items-center justify-center rounded-base border-2 border-border bg-main text-main-foreground shadow-shadow *:[svg:not([class*='size-'])]:size-6",
className,
)}
{...props}
/>
);
}
function AlertDialogTitle({ className, ...props }: AlertDialogPrimitive.Title.Props) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
{...props}
className={(state) =>
cn("font-heading text-lg", typeof className === "function" ? className(state) : className)
}
/>
);
}
function AlertDialogDescription({ className, ...props }: AlertDialogPrimitive.Description.Props) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
{...props}
className={(state) =>
cn(
"text-sm font-base text-foreground",
typeof className === "function" ? className(state) : className,
)
}
/>
);
}
function AlertDialogAction({
render,
size = "default",
variant = "default",
...props
}: AlertDialogButtonProps) {
return (
<AlertDialogPrimitive.Close
data-slot="alert-dialog-action"
render={render ?? <Button variant={variant} size={size} />}
{...props}
/>
);
}
function AlertDialogCancel({
render,
size = "default",
variant = "neutral",
...props
}: AlertDialogButtonProps) {
return (
<AlertDialogPrimitive.Close
data-slot="alert-dialog-cancel"
render={render ?? <Button variant={variant} size={size} />}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};
#Usage
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
export default function ConfirmArchive({ onArchive }: { onArchive: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}>Archive project</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Archive this project?</AlertDialogTitle>
<AlertDialogDescription>
The project will leave the active list. You can restore it later.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep project</AlertDialogCancel>
<AlertDialogAction onClick={onArchive}>Archive project</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
AlertDialog.open / onOpenChange | Controlled modal state | Uncontrolled |
AlertDialog.defaultOpen | Initial visibility | false |
AlertDialogTrigger.render | Compose a trigger button | Primitive button |
AlertDialogTitle / AlertDialogDescription | Accessible name and consequence | — |
AlertDialogCancel | Cancel and restore focus | Styled close button |
AlertDialogAction | Confirm and close | Styled close button |
AlertDialogContent.size | Dialog width: default or sm | default |
AlertDialogAction.size / variant | Confirmation button size and appearance | default / default |
AlertDialogCancel.size / variant | Cancellation button size and appearance | default / neutral |
#Accessibility
Keep a meaningful title and description. The modal traps keyboard focus and normally returns focus to its trigger. Cancel must be easy to identify. Closing the dialog does not prove that a server operation succeeded: for asynchronous work, control open, show failure inside the dialog, and close only after success.