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

Source: https://designrevision.com/components/alert-dialog

The **Shadcn Alert Dialog** is the interrupting confirmation — the canonical <a href="https://ui.shadcn.com/docs/components/alert-dialog" rel="nofollow">shadcn/ui</a> alert dialog built on <a href="https://www.radix-ui.com/primitives/docs/components/alert-dialog" rel="nofollow">Radix</a>, 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](/components/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.

## Installation

### alert-dialog

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog
```

**Dependencies:** @radix-ui/react-alert-dialog, utils, button

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| open | boolean | — | Controlled open state (on AlertDialog). Pair with onOpenChange. |
| onOpenChange | (open: boolean) => void | — | Fires when the dialog opens or closes (on AlertDialog). |

**Usage & accessibility:** 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'})}.

```tsx
// components/ui/alert-dialog.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog-demo-01
```

**Dependencies:** alert-dialog, button

```tsx
// components/ui/alert-dialog-demo-01.tsx
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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog-destructive-01
```

**Dependencies:** alert-dialog, button

```tsx
// components/ui/alert-dialog-destructive-01.tsx
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.

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog-async-01
```

**Dependencies:** lucide-react, alert-dialog, button

```tsx
// components/ui/alert-dialog-async-01.tsx
"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`.

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog-confirm-input-01
```

**Dependencies:** alert-dialog, button, input, label

```tsx
// components/ui/alert-dialog-confirm-input-01.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog-controlled-01
```

**Dependencies:** alert-dialog, button

```tsx
// components/ui/alert-dialog-controlled-01.tsx
"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.

**Install:**

```bash
npx shadcn@latest add @designrevision/alert-dialog-icon-01
```

**Dependencies:** lucide-react, alert-dialog, button

```tsx
// components/ui/alert-dialog-icon-01.tsx
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&apos;s access immediately. They&apos;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>
  )
}
```
