Alert Dialog
A modal confirmation that interrupts people before a consequential or irreversible action, and can only be dismissed by making a choice.
"use client"
import * as React from "react"
import { Trash2Icon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export default function AlertDialogDemo() {
const [open, setOpen] = React.useState(false)
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger render={<Button variant="outline" />}>
<Trash2Icon data-icon="inline-start" />
Delete project
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia variant="destructive">
<Trash2Icon />
</AlertDialogMedia>
<AlertDialogTitle>Delete “Atlas”?</AlertDialogTitle>
<AlertDialogDescription>
This permanently deletes the project, its 142 issues and all
deployments. This can’t be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep project</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
onClick={() => setOpen(false)}
>
Delete project
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/alert-dialog.jsonUsage
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"const [open, setOpen] = React.useState(false)
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger render={<Button variant="outline" />}>Delete project</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete “Atlas”?</AlertDialogTitle>
<AlertDialogDescription>This can’t be undone.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep project</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={deleteProject}>
Delete project
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>AlertDialogCancel closes the dialog. AlertDialogAction is a plain Button, on purpose: the confirmed work is often asynchronous, and the dialog should stay open — with the action in its loading state — until it succeeds or fails. Control open and close it yourself when the work is done.Examples
Basic
A title and a description are often enough. The description says what will be lost; the buttons say what each choice does.
"use client"
import * as React 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 AlertDialogBasic() {
const [open, setOpen] = React.useState(false)
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger render={<Button variant="ghost" />}>
Discard draft
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Discard unsaved changes?</AlertDialogTitle>
<AlertDialogDescription>
You edited this post 4 minutes ago. If you leave now, those edits
will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Keep editing</AlertDialogCancel>
<AlertDialogAction onClick={() => setOpen(false)}>
Discard changes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Media
AlertDialogMedia adds an icon that sets the tone before the title is read. Match its variant to the consequence — destructive for data loss, warning for disruption, success to confirm something good that can't be taken back, brand for significant but safe actions, default for everything else — and use the same emphasis on the action button.
"use client"
import * as React from "react"
import { GitBranchIcon, LogOutIcon, RocketIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
const confirmations = [
{
trigger: "Sign out everywhere",
media: "default",
icon: LogOutIcon,
title: "Sign out of all devices?",
description:
"You’ll be signed out on 4 devices, including this one. You’ll need your password to sign back in.",
action: "Sign out everywhere",
actionVariant: "default",
},
{
trigger: "Disconnect GitHub",
media: "warning",
icon: GitBranchIcon,
title: "Disconnect GitHub?",
description:
"New commits will stop triggering deployments. Existing deployments stay online.",
action: "Disconnect",
actionVariant: "default",
},
{
trigger: "Deploy to production",
media: "brand",
icon: RocketIcon,
title: "Deploy to production?",
description:
"Commit 7f3a2c1 will replace the live version for 12,408 active users. You can roll back at any time.",
action: "Deploy",
actionVariant: "brand",
},
] as const
function Confirmation({ item }: { item: (typeof confirmations)[number] }) {
const [open, setOpen] = React.useState(false)
const Icon = item.icon
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger render={<Button variant="outline" />}>
{item.trigger}
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia variant={item.media}>
<Icon />
</AlertDialogMedia>
<AlertDialogTitle>{item.title}</AlertDialogTitle>
<AlertDialogDescription>{item.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
variant={item.actionVariant}
onClick={() => setOpen(false)}
>
{item.action}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
export default function AlertDialogMediaVariants() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
{confirmations.map((item) => (
<Confirmation key={item.trigger} item={item} />
))}
</div>
)
}Small
size="sm" centers the content and splits the footer into two equal buttons. Use it for short, binary questions — permissions and quick opt-ins — especially on mobile.
"use client"
import * as React from "react"
import { BellRingIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export default function AlertDialogSmall() {
const [open, setOpen] = React.useState(false)
return (
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger render={<Button variant="outline" />}>
Enable alerts
</AlertDialogTrigger>
<AlertDialogContent size="sm">
<AlertDialogHeader>
<AlertDialogMedia variant="brand">
<BellRingIcon />
</AlertDialogMedia>
<AlertDialogTitle>Turn on build alerts?</AlertDialogTitle>
<AlertDialogDescription>
We’ll notify you when a deployment fails.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Not now</AlertDialogCancel>
<AlertDialogAction onClick={() => setOpen(false)}>
Turn on
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Typed confirmation
For actions that destroy a lot of work at once, ask people to type the name of what they are deleting. The action stays disabled until the text matches, which prevents confirming out of habit.
"use client"
import * as React from "react"
import { TriangleAlertIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
const workspace = "acme-production"
export default function AlertDialogTypedConfirmation() {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("")
const matches = value === workspace
return (
<AlertDialog
open={open}
onOpenChange={(next) => {
setOpen(next)
if (!next) setValue("")
}}
>
<AlertDialogTrigger render={<Button variant="destructive" />}>
Delete workspace
</AlertDialogTrigger>
<AlertDialogContent>
<form
className="flex flex-col gap-5"
onSubmit={(event) => {
event.preventDefault()
if (matches) setOpen(false)
}}
>
<AlertDialogHeader>
<AlertDialogMedia variant="destructive">
<TriangleAlertIcon />
</AlertDialogMedia>
<AlertDialogTitle>Delete this workspace?</AlertDialogTitle>
<AlertDialogDescription>
All 38 projects, 214 members and their history will be removed
permanently.
</AlertDialogDescription>
</AlertDialogHeader>
<Field>
<FieldLabel
htmlFor="confirm-workspace"
className="font-normal text-muted-foreground"
>
<span>
Type{" "}
<strong className="font-mono font-medium text-foreground">
{workspace}
</strong>{" "}
to confirm
</span>
</FieldLabel>
<Input
id="confirm-workspace"
value={value}
autoComplete="off"
spellCheck={false}
onChange={(event) => setValue(event.target.value)}
/>
</Field>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
type="submit"
variant="destructive"
disabled={!matches}
>
Delete workspace
</AlertDialogAction>
</AlertDialogFooter>
</form>
</AlertDialogContent>
</AlertDialog>
)
}Async action
Keep the dialog open while the request runs: put the action in its loading state, disable cancel, and ignore dismissal until the work settles. Then close the dialog and confirm the result with a Toast.
"use client"
import * as React from "react"
import { KeyRoundIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"
export default function AlertDialogAsync() {
const [open, setOpen] = React.useState(false)
const [pending, setPending] = React.useState(false)
async function revoke() {
setPending(true)
// Keep the dialog open until the request settles.
await new Promise((resolve) => setTimeout(resolve, 1500))
setPending(false)
setOpen(false)
toast.add({ title: "API key revoked", type: "success" })
}
return (
<AlertDialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
<AlertDialogTrigger render={<Button variant="outline" />}>
Revoke key
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogMedia variant="warning">
<KeyRoundIcon />
</AlertDialogMedia>
<AlertDialogTitle>Revoke “CI deploy key”?</AlertDialogTitle>
<AlertDialogDescription>
Pipelines using this key will fail on their next run. You can create
a new key afterwards.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
<AlertDialogAction
variant="destructive"
loading={pending}
onClick={revoke}
>
Revoke key
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}<AlertDialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
{/* … */}
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive" loading={pending} onClick={revoke}>
Revoke key
</AlertDialogAction>
</AlertDialog>Guidelines
When to use
- Before destructive actions that can't be undone: deleting, revoking, permanently removing people.
- Before actions with a large blast radius: deploying to production, disconnecting an integration, signing out everywhere.
- When leaving would lose work that can't be recovered.
When not to use
- For actions that can be undone — act immediately and offer Undo in a Toast. An undo is always kinder than a confirmation.
- To collect input or show information — use a Dialog.
- To tell people something went wrong — use an inline Alert or a toast.
Confirm rarely
Every confirmation trains people to click through the next one. Reserve alert dialogs for the few moments that deserve them, and make those moments specific: name the thing, the number, and the consequence.
Write the choices
The title asks one question. The action repeats the verb from the title, and the cancel button says what staying means (Keep project), or is a plain Cancel when nothing more specific fits. Never use Yes, No or OK.
Order and emphasis
Put the action last — on the right from sm up, on top when the footer stacks on mobile — and cancel before it. Use the destructive button only for destructive actions; the red means you are about to lose something.
Accessibility
The alert dialog follows the WAI-ARIA alert dialog pattern. Base UI renders it with role="alertdialog", labels it with the title and describes it with the description, so screen readers announce the consequence as soon as it opens.
| Key | Behavior |
|---|---|
EnterSpace | On the trigger: opens the alert dialog. |
Tab | Moves between the choices. Focus is trapped inside the dialog. |
ShiftTab | Moves focus backwards, wrapping at the start. |
Esc | Closes the dialog, the same as choosing Cancel, and returns focus to the trigger. |
- No outside dismissal. Clicking the scrim doesn't close an alert dialog: people must choose. Esc still works as Cancel.
- Title and description are required. They are the only way a screen reader user learns what they are confirming.
- Focus. Focus moves to the first focusable element — usually the cancel button, the safe choice — and returns to the trigger afterwards.
- Color is never the only signal. The destructive action is red, and its label also says Delete.
API reference
AlertDialog
The root. Doesn't render an element. Accepts every prop of Base UI AlertDialog.Root — the same as Dialog, minus modal and disablePointerDismissal, which are always on.
AlertDialogContent
Renders the portal, scrim and popup. The two wrappers it composes, AlertDialogPortal and AlertDialogOverlay, are also exported for layouts that place them by hand.
AlertDialogMedia
An icon badge shown beside the title from the sm breakpoint up, and above it on small dialogs and on narrower screens.
AlertDialogAction
A prfct Button. Accepts every Button prop — variant, loading, disabled — and does not close the dialog.
AlertDialogCancel
Closes the dialog. Renders a prfct Button.
AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter
Layout parts. AlertDialogTitle renders an <h2> and names the dialog; AlertDialogDescription renders a <p> and describes it. AlertDialogFooter stacks the choices on small screens and right-aligns them from sm up.