Alert Dialog
A confirmation dialog for destructive or irreversible actions: no close X, no click-outside dismiss, and the cancel button is focused first.
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() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" tone="danger">
Delete project
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. All deployments and logs will be removed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/alert-dialogDependencies, the @ai2/tokens theme and the component file are installed together. @ai2/button installs alongside, since the action and cancel buttons reuse its variants.
Install dependencies
npm install radix-ui@^1.6.1 tw-animate-css@^1.4.0Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/alert-dialog.tsx"use client"
import type * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "@/components/ui/primitives"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
function AlertDialog(props: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger(
props: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>
) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPrimitive.Portal>
<AlertDialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 motion-reduce:animate-none" />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
/* The scroll-container rationale lives in dialog.tsx; alert-dialog
had the same structure and the same defect (measured:
max-height none, overflow-y visible). The risk is higher here:
an alert dialog's confirm button is by definition at the
bottom. */
"fixed left-1/2 top-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 overscroll-contain sm:max-w-md rounded-xl border border-border bg-popover p-6 text-popover-foreground shadow-lg duration-(--motion-base) data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 motion-reduce:animate-none",
className
)}
{...props}
/>
</AlertDialogPrimitive.Portal>
)
}
function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-1.5 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-2 sm:flex-row sm:justify-end", className)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold leading-none", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
tone = "danger",
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> & {
tone?: "neutral" | "brand" | "danger"
}) {
return (
<AlertDialogPrimitive.Action
data-slot="alert-dialog-action"
className={cn(buttonVariants({ variant: "solid", tone, size: "md" }), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
data-slot="alert-dialog-cancel"
className={cn(buttonVariants({ variant: "outline", tone: "neutral", size: "md" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}Alert Dialog imports buttonVariants from the Button component, so copy that file too. Manual installs also skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Usage
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" tone="danger">Delete project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>Unlike Dialog, there is no close X and clicking outside does not dismiss. The user must choose AlertDialogCancel or AlertDialogAction.
Examples
Destructive confirmation
The action button defaults to tone="danger", so no prop is needed for the destructive case.
Non-destructive action
For irreversible but non-destructive confirmations, set tone="brand" (or "neutral") on AlertDialogAction.
Action tones
All three values of the tone axis on AlertDialogAction: open each dialog to compare the action button. The default is danger.
Props
AlertDialogAction adds one prop; every part also forwards its underlying radix props, e.g. open / onOpenChange on AlertDialog.
AlertDialogAction props
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "neutral" | "brand" | "danger" | "danger" | Semantic color of the action button. Styled as a solid medium button via buttonVariants. |
ai2 Alert Dialog: destructive confirmations done right in React
The ai2 Alert Dialog is a shadcn-compatible react alert dialog, built on the radix-ui AlertDialog primitive and styled with Tailwind CSS v4. It exists for one job: confirming destructive or irreversible actions. Delete a project, revoke a key, cancel a subscription: the dialog interrupts, explains the consequence and forces an explicit choice between cancel and confirm.
It ships through the shadcn registry format, so you install it with one CLI command, an MCP agent, or a copy-paste, and the TypeScript source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Alert Dialog?
It is a nine-part composition matching shadcn/ui: AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction and AlertDialogCancel.
Unlike a regular dialog, there is no close X and clicking the overlay does not dismiss it: the user must pick a button. The footer buttons are styled through the shared buttonVariants function from the ai2 Button: the cancel button is a neutral outline button, and the action button is a solid button whose tone defaults to danger, so the destructive case needs zero extra props.
Why use it
- Danger is the default: AlertDialogAction defaults to tone="danger", so the most common case, a red destructive confirm button, is the zero-config path instead of an override.
- No accidental dismissal: The radix AlertDialog primitive intentionally omits outside-click dismissal, so a stray click cannot silently cancel (or worse, skip) a decision about data loss.
- Cancel is focused first: Per the WAI-ARIA alertdialog pattern, initial focus lands on the cancel button, so pressing Enter reflexively does not destroy anything.
- Buttons match your system: Action and cancel reuse buttonVariants from the ai2 Button, so dialog buttons are pixel-identical to every other button in your product.
- Agent-readable metadata: The registry item describes the parts and the danger default in plain words, so an MCP agent can wire a correct destructive confirmation without guessing.
Features
- Radix AlertDialog primitive: Focus trap, role="alertdialog", aria-labelledby and aria-describedby wiring, Escape handling and scroll lock come from radix-ui.
- Tone-aware action button: AlertDialogAction accepts tone="neutral" | "brand" | "danger" and defaults to danger; it renders as a solid medium button via buttonVariants.
- Token-driven motion: Open and close animate with fade and zoom using the shared --motion-base duration token, matching the rest of ai2.
- Responsive footer: AlertDialogFooter stacks buttons in reverse column order on small screens and aligns them to the end in a row on larger ones.
- Composable trigger: AlertDialogTrigger supports asChild, so any ai2 Button (or other element) opens the dialog without wrapper markup.
- Data attributes for styling: Every part exposes data-slot (alert-dialog, alert-dialog-content, alert-dialog-action and so on) for targeted CSS overrides without forking.
Production tips
- Name the object in the title: "Delete this project?" beats "Are you sure?". Users skimming a focused dialog should know exactly what is at stake from the title alone.
- State the consequence in the description: One sentence on what happens and whether it is reversible: "Requests using this key will start failing immediately." Skip boilerplate.
- Label the action with a verb, not OK: "Delete", "Revoke", "Publish". A verb label plus the danger tone makes the button self-describing even out of context.
- Match the action tone to the risk: Keep the danger default for destructive actions; switch to tone="brand" or "neutral" for irreversible but constructive ones like publishing, so red keeps meaning danger.
- Do not overuse it: Interrupting confirmation is for irreversible actions. For reversible ones, prefer doing the action immediately and offering undo via a toast.
Works with the rest of ai2
The natural trigger is an ai2 Button (often soft or outline danger) via asChild, and destructive entries in an ai2 Dropdown Menu or row actions in an ai2 Table commonly open one before anything is deleted.
For non-blocking flows, use the ai2 Dialog instead: it has a close X and outside-click dismissal. After the action completes, confirm the result with an ai2 Sonner toast. All three share the same tokens, so the flow stays visually coherent in both modes.