@cf/alert-dialog
Alert Dialog
A modal confirmation that interrupts for a decision, usually before something destructive, and can't be dismissed by clicking outside.
Delete keeps the dialog open with both buttons disabled for a moment, then closes it through open, the same way the notes delete does.
Base UI's AlertDialog. It shares the dialog's overlay, scroll cap and footer band, and adds AlertDialogMedia for an icon beside the title.
The thing to know is that AlertDialogAction doesn't close anything. That matches upstream and suits the delete flow in notes, which keeps the dialog open with both buttons disabled until the delete finishes, then closes it through open. If you want an action that closes on click, you have to do it yourself.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
<AlertDialog onOpenChange={setOpen} open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this note?</AlertDialogTitle>
<AlertDialogDescription>This can't be undone.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
<AlertDialogAction disabled={pending} onClick={remove} variant="destructive">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>Props
| Prop | Type | Default |
|---|---|---|
| AlertDialogContent size | default | sm | default |
| AlertDialogCancel variant | Button variant | outline |
| AlertDialogCancel size | Button size | default |
Rules
The same rules ship in DESIGN.md and the MCP server, for the agents building with this.
- must
Include
AlertDialogTitleandAlertDialogDescriptioninAlertDialogHeader, and say in the description what will be lost.title-and-description
- must
Always render an
AlertDialogCancel; it's the only built-in part that closes the dialog.cancel-is-a-way-out
- must
Close the dialog yourself from
AlertDialogAction'sonClick(for example with a controlledopen), because it's a plainButton, not a close part.action-closes-itself-manually
- should
Give a delete or discard
AlertDialogActionvariant="destructive", and disable both buttons while the action is pending.destructive-action-variant
- may
Use
size="sm"onAlertDialogContentfor a short confirmation; it centres the header and puts the two footer buttons side by side in a grid.sm-for-two-buttons
Install
bunx shadcn@latest add @cf/alert-dialogNeeds the @cf registry in your components.json once. The theme and any sibling components come along automatically.
Source
alert-dialog.tsxShowHide
"use client";
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import { cn } from "cn";
import type * as React from "react";
import { Button } from "./button";
function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...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
className={cn(
"data-open:fade-in-0 data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-foreground/10 duration-100 data-closed:animate-out data-open:animate-in supports-backdrop-filter:backdrop-blur-xs",
className
)}
data-slot="alert-dialog-overlay"
{...props}
/>
);
}
function AlertDialogContent({
className,
size = "default",
...props
}: AlertDialogPrimitive.Popup.Props & {
size?: "default" | "sm";
}) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Popup
className={cn(
"group/alert-dialog-content data-open:fade-in-0 data-open:zoom-in-95 data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 grid max-h-[calc(100svh-2rem)] w-full -translate-x-1/2 -translate-y-1/2 gap-4 overflow-y-auto overscroll-contain rounded-xl bg-popover p-4 text-popover-foreground outline-none ring-1 ring-foreground/10 duration-100 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-closed:animate-out data-open:animate-in data-[size=default]:sm:max-w-sm",
className
)}
data-size={size}
data-slot="alert-dialog-content"
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
className
)}
data-slot="alert-dialog-header"
{...props}
/>
);
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
className
)}
data-slot="alert-dialog-footer"
{...props}
/>
);
}
function AlertDialogMedia({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
className
)}
data-slot="alert-dialog-media"
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
className={cn(
"font-medium text-base sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
className
)}
data-slot="alert-dialog-title"
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
className={cn(
"text-balance text-muted-foreground text-sm md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
data-slot="alert-dialog-description"
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof Button>) {
return (
<Button
className={cn(className)}
data-slot="alert-dialog-action"
{...props}
/>
);
}
function AlertDialogCancel({
className,
variant = "outline",
size = "default",
...props
}: AlertDialogPrimitive.Close.Props &
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
return (
<AlertDialogPrimitive.Close
className={cn(className)}
data-slot="alert-dialog-cancel"
render={<Button size={size} variant={variant} />}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogOverlay,
AlertDialogPortal,
AlertDialogTitle,
AlertDialogTrigger,
};