# Alert Dialog

> Confirmation dialog for destructive actions; no backdrop dismiss and focus starts on Cancel.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/alert-dialog
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/alert-dialog.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/alert-dialog-demo.tsx"
"use client"

import * as React from "react"
import { Trash2 } from "lucide-react"

import { AlertDialog } from "@/components/alert-dialog"

export default function AlertDialogDemo() {
  const [open, setOpen] = React.useState(false)
  const [deleted, setDeleted] = React.useState(false)

  return (
    <div className="grid justify-items-center gap-4">
      <button
        type="button"
        onClick={() => setOpen(true)}
        disabled={deleted}
        className="inline-flex h-9 items-center gap-2 rounded-lg bg-destructive px-4 text-sm font-medium text-white transition-colors hover:bg-destructive/90 dark:bg-red-600 dark:hover:bg-red-700 disabled:opacity-50"
      >
        <Trash2 className="size-4" /> {deleted ? "Project deleted" : "Delete project"}
      </button>
      {deleted && (
        <button type="button" onClick={() => setDeleted(false)} className="text-xs text-muted-foreground underline underline-offset-4">
          Restore
        </button>
      )}
      <AlertDialog
        open={open}
        onOpenChange={setOpen}
        destructive
        icon={<Trash2 className="size-5" />}
        title="Delete this project?"
        description="All deployments and environment variables will be removed. This action cannot be undone."
        actionLabel="Delete project"
        onAction={() => setDeleted(true)}
      />
    </div>
  )
}
```

## Source

```tsx title="components/alert-dialog.tsx"
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"

export interface AlertDialogProps {
  open: boolean
  onOpenChange: (open: boolean) => void
  title: React.ReactNode
  description: React.ReactNode
  cancelLabel?: string
  actionLabel?: string
  /** Style the action as destructive */
  destructive?: boolean
  /** Optional icon shown above the title */
  icon?: React.ReactNode
  onAction?: () => void
}

// Interrupts the user and needs an explicit answer: no backdrop dismiss, Cancel takes focus first.
export function AlertDialog({
  open,
  onOpenChange,
  title,
  description,
  cancelLabel = "Cancel",
  actionLabel = "Continue",
  destructive = false,
  icon,
  onAction,
}: AlertDialogProps) {
  const ref = React.useRef<HTMLDialogElement>(null)
  const titleId = React.useId()
  const descriptionId = React.useId()

  React.useEffect(() => {
    const dialog = ref.current
    if (!dialog) return
    if (open && !dialog.open) dialog.showModal()
    else if (!open && dialog.open) dialog.close()
  }, [open])

  return (
    <dialog
      ref={ref}
      role="alertdialog"
      aria-labelledby={titleId}
      aria-describedby={descriptionId}
      onCancel={(event) => {
        event.preventDefault()
        onOpenChange(false)
      }}
      className={cn(
        "m-auto w-[calc(100%-2rem)] max-w-md rounded-2xl border bg-background p-6 text-foreground shadow-2xl",
        "scale-95 opacity-0 transition-[opacity,scale,display,overlay] transition-discrete duration-200 ease-out open:scale-100 open:opacity-100 starting:open:scale-95 starting:open:opacity-0",
        "backdrop:bg-black/0 backdrop:transition-[background-color,display,overlay] backdrop:transition-discrete backdrop:duration-200 open:backdrop:bg-black/50 starting:open:backdrop:bg-black/0"
      )}
    >
      <div className="flex flex-col gap-4">
        {icon && (
          <span
            className={cn(
              "flex size-10 items-center justify-center rounded-full [&_svg]:size-5",
              destructive ? "bg-destructive/10 text-destructive" : "bg-primary/10 text-primary"
            )}
          >
            {icon}
          </span>
        )}
        <div className="flex flex-col gap-1.5">
          <h2 id={titleId} className="text-lg leading-tight font-semibold">
            {title}
          </h2>
          <p id={descriptionId} className="text-sm text-muted-foreground">
            {description}
          </p>
        </div>
        <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
          <button
            type="button"
            autoFocus
            onClick={() => onOpenChange(false)}
            className="inline-flex h-9 items-center justify-center rounded-lg border bg-background px-4 text-sm font-medium transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none"
          >
            {cancelLabel}
          </button>
          <button
            type="button"
            onClick={() => {
              onAction?.()
              onOpenChange(false)
            }}
            className={cn(
              "inline-flex h-9 items-center justify-center rounded-lg px-4 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
              destructive ? "bg-destructive text-white hover:bg-destructive/90 dark:bg-red-600 dark:hover:bg-red-700" : "bg-primary text-primary-foreground hover:opacity-90"
            )}
          >
            {actionLabel}
          </button>
        </div>
      </div>
    </dialog>
  )
}
```
