Shadcn Alert Dialog
Free shadcn/ui alert dialog component for React — destructive confirmations, async actions with a spinner, type-to-confirm, and controlled open. The examples official shadcn never shipped.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
Props
open
= —
boolean
Controlled open state (on AlertDialog). Pair with onOpenChange.
onOpenChange
= —
(open: boolean) => void
Fires when the dialog opens or closes (on AlertDialog).
Eleven parts; AlertDialogAction and AlertDialogCancel are pre-styled with buttonVariants() (primary) and buttonVariants({variant:'outline'}). No close X. Closes on Escape but NOT on outside-click by design. Make the action destructive with className={buttonVariants({variant:'destructive'})}.
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
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 AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</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-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", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
Examples
Basic
A confirmation with a `AlertDialogCancel` (outline) and `AlertDialogAction` (primary) — the canonical interruptive prompt.
Packages
Props
No props documented yet.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export default function AlertDialogDemo01() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Show dialog</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
Destructive
A delete confirmation with a **red** action — `className={buttonVariants({ variant: "destructive" })}` on `AlertDialogAction`.
Packages
Props
No props documented yet.
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button, buttonVariants } from "@/components/ui/button"
export default function AlertDialogDestructive01() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete project</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes the project and all of its data. This
action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
Async action
Keeps the dialog **open** while the action runs — `preventDefault()` on the action, a `Loader2` spinner, disabled buttons, then close on success.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import { Loader2Icon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button, buttonVariants } from "@/components/ui/button"
export default function AlertDialogAsync01() {
const [open, setOpen] = React.useState(false)
const [loading, setLoading] = React.useState(false)
const onConfirm = async () => {
setLoading(true)
await new Promise((resolve) => setTimeout(resolve, 1500))
setLoading(false)
setOpen(false)
}
return (
<AlertDialog open={open} onOpenChange={(next) => !loading && setOpen(next)}>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete account</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete account?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes your account. The dialog stays open while
the request is in flight.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={loading}
onClick={(event) => {
event.preventDefault()
onConfirm()
}}
className={buttonVariants({ variant: "destructive" })}
>
{loading ? (
<>
<Loader2Icon className="animate-spin" />
Deleting…
</>
) : (
"Delete"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
Type to confirm
A GitHub-style guard — the destructive action stays disabled until the user types `DELETE`.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button, buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function AlertDialogConfirmInput01() {
const [value, setValue] = React.useState("")
const confirmed = value === "DELETE"
return (
<AlertDialog onOpenChange={() => setValue("")}>
<AlertDialogTrigger asChild>
<Button variant="destructive">Delete repository</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete repository</AlertDialogTitle>
<AlertDialogDescription>
This cannot be undone. Type{" "}
<span className="font-medium text-foreground">DELETE</span> to
confirm.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="grid gap-2">
<Label htmlFor="confirm" className="sr-only">
Type DELETE to confirm
</Label>
<Input
id="confirm"
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="DELETE"
autoComplete="off"
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
disabled={!confirmed}
className={buttonVariants({ variant: "destructive" })}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
Controlled
Opened from React state with no `AlertDialogTrigger` — for confirming an action initiated elsewhere.
Packages
Props
No props documented yet.
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export default function AlertDialogControlled01() {
const [open, setOpen] = React.useState(false)
return (
<>
<Button variant="outline" onClick={() => setOpen(true)}>
Leave page
</Button>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Discard changes?</AlertDialogTitle>
<AlertDialogDescription>
You have unsaved changes. Leaving now will discard them. This
dialog is opened from state, with no AlertDialogTrigger.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep editing</AlertDialogCancel>
<AlertDialogAction>Discard</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}
With icon
A centered warning-icon header for the highest-stakes confirmations.
Packages
Props
No props documented yet.
import { TriangleAlertIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button, buttonVariants } from "@/components/ui/button"
export default function AlertDialogIcon01() {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline">Revoke access</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader className="items-center text-center sm:text-center">
<div className="flex size-11 items-center justify-center rounded-full bg-destructive/10">
<TriangleAlertIcon className="size-5 text-destructive" />
</div>
<AlertDialogTitle>Revoke access?</AlertDialogTitle>
<AlertDialogDescription>
This removes the member's access immediately. They'll need
a new invite to return.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="sm:justify-center">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className={buttonVariants({ variant: "destructive" })}>
Revoke
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
The Shadcn Alert Dialog is the interrupting confirmation — the canonical shadcn/ui alert dialog built on Radix, with AlertDialogAction and AlertDialogCancel pre-styled from your buttonVariants. Official ships it with zero examples; the ones below cover what you actually need — destructive deletes, async actions, and type-to-confirm guards.
When to use — Alert Dialog vs Dialog
Use an Alert Dialog when an action needs a deliberate yes/no and shouldn't be dismissed by accident — delete, discard, sign out, revoke. It has no close X, doesn't close on an outside click, and is announced as an alert. Use a Dialog for general modal content (forms, details) — it has a close X and dismisses on outside-click and Escape. If you find yourself wanting an alert dialog to close on outside click, you want a Dialog.
Destructive actions
The most common alert dialog is a destructive confirmation. The action button defaults to your primary color; make it red by passing the destructive variant: className={buttonVariants({ variant: "destructive" })} on AlertDialogAction. Keep the title a plain question ("Delete this project?") and the description specific about consequences.
Async actions
By default the action closes the dialog the instant it's clicked — wrong when the work is async. Control the open state, call event.preventDefault() in the action's onClick, run the request, and setOpen(false) only on success — disabling both buttons and showing a spinner in between (and blocking Escape while in flight). The Async action example is the complete pattern.
Type-to-confirm
For irreversible, high-blast-radius actions, gate the action behind a verification phrase: keep AlertDialogAction disabled until the user types the exact value (e.g. DELETE). It's the GitHub "type the repo name" pattern, and it's a few lines — see the Type-to-confirm example.
Triggering & the dropdown gotcha
Open it declaratively with AlertDialogTrigger, or controlled from state (no trigger) to confirm an action started elsewhere. A frequent bug — "the alert dialog closes immediately" — happens when the trigger is nested inside a DropdownMenuItem: the menu closing steals focus and dismisses the dialog. The fix is to control the alert dialog's open and set it from the menu item's onSelect, rendering the dialog as a sibling rather than nesting the trigger.
Accessibility
Radix gives the alert dialog role="alertdialog", traps focus, moves initial focus to the Cancel button (the safe default), and returns focus to the trigger on close. Always include an AlertDialogTitle and AlertDialogDescription so the prompt is announced. Escape closes it; outside-click does not.
Theming
The alert dialog reads your design tokens (--background, --border, --destructive) and the shared buttonVariants, so it follows your theme — including dark mode — with zero overrides.