Skip to content

Alert Dialog

A modal confirmation that interrupts people before a consequential or irreversible action, and can only be dismissed by making a choice.

SourceBase UIRegistry
"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.json

Usage

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>
The action doesn’t close the dialog
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.

Delete 3 files?They’ll be gone for everyone.
DoThe question and the buttons use the same verb.
Are you sure?This action is permanent.
Don’t“Yes” and “No” make people reread the question under pressure.

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.

KeyBehavior
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.

PropTypeDefault
open

Whether the alert dialog is open. Control it when the action is asynchronous.

booleanNo default
defaultOpen

Whether it is open initially, when uncontrolled.

booleanfalse
onOpenChange

Called when it opens or closes.

(open: boolean, details) => voidNo default
onOpenChangeComplete

Called after the enter or exit animation finishes.

(open: boolean) => voidNo default
actionsRef

Imperative handle to close or unmount the dialog.

RefObject<{ close, unmount }>No default

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.

PropTypeDefault
size

sm centers the content, narrows the dialog and splits the footer into two equal buttons.

"default" | "sm""default"
initialFocus

Element to focus when it opens.

boolean | RefObject | (interaction) => HTMLElement | booleanNo default
finalFocus

Element to focus when it closes.

boolean | RefObject | (interaction) => HTMLElement | booleanNo default

AlertDialogMedia

An icon badge shown beside the title from the sm breakpoint up, and above it on small dialogs and on narrower screens.

PropTypeDefault
variant

Color of the badge. Match it to the consequence of the action.

"default" | "destructive" | "warning" | "success" | "brand""default"

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.

PropTypeDefault
variant

Visual emphasis of the button.

Button variant"outline"
size

Size of the button.

Button size"default"

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.