Alert Dialog

A confirmation dialog that interrupts until the user accepts or cancels. Backdrop clicks do not dismiss it. Only Escape or an explicit action will.

Default

import { Trash2Icon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"

export default function Example() {
  return (
    <AlertDialog>
      <AlertDialogTrigger
        render={
          <Button variant="danger">
            <Trash2Icon />
            Discard Flight Log
          </Button>
        }
      />
      <AlertDialogPortal>
        <AlertDialogBackdrop />
        <AlertDialogPopup>
          <AlertDialogTitle>Discard flight log?</AlertDialogTitle>
          <AlertDialogDescription>
            This mission data can't be recovered once you leave orbit.
          </AlertDialogDescription>
          <div className="mt-6 flex justify-end gap-2">
            <AlertDialogClose render={<Button variant="outline">Cancel</Button>} />
            <AlertDialogClose render={<Button variant="danger">Discard</Button>} />
          </div>
        </AlertDialogPopup>
      </AlertDialogPortal>
    </AlertDialog>
  )
}

Anatomy

import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"

<AlertDialog>
  <AlertDialogTrigger />
  <AlertDialogPortal>
    <AlertDialogBackdrop />
    <AlertDialogPopup>
      <AlertDialogTitle />
      <AlertDialogDescription />
      <AlertDialogClose />
    </AlertDialogPopup>
  </AlertDialogPortal>
</AlertDialog>

With icon

Compose a decorative icon beside the title when the action needs a stronger visual cue. Keep severity in the title text and hide the icon from assistive tech.

import { CircleAlertIcon, DropletsIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"

export default function Example() {
  return (
    <AlertDialog>
      <AlertDialogTrigger
        render={
          <Button variant="danger">
            <DropletsIcon />
            Purge Fuel Tank
          </Button>
        }
      />
      <AlertDialogPortal>
        <AlertDialogBackdrop />
        <AlertDialogPopup>
          <div className="flex items-start gap-3">
            <CircleAlertIcon
              aria-hidden="true"
              className="mt-0.5 size-5 shrink-0 text-danger"
            />
            <div>
              <AlertDialogTitle>Purge the fuel tank?</AlertDialogTitle>
              <AlertDialogDescription>
                Every drop vents to atmosphere. Refueling takes another two
                orbits.
              </AlertDialogDescription>
            </div>
          </div>
          <div className="mt-6 flex justify-end gap-2">
            <AlertDialogClose render={<Button variant="outline">Cancel</Button>} />
            <AlertDialogClose render={<Button variant="danger">Purge</Button>} />
          </div>
        </AlertDialogPopup>
      </AlertDialogPortal>
    </AlertDialog>
  )
}

With tertiary action

Place an optional ghost action on the left. Keep Cancel and the primary confirm grouped on the right.

import { FilePenLineIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"

export default function Example() {
  return (
    <AlertDialog>
      <AlertDialogTrigger
        render={
          <Button variant="danger">
            <FilePenLineIcon />
            Overwrite Telemetry
          </Button>
        }
      />
      <AlertDialogPortal>
        <AlertDialogBackdrop />
        <AlertDialogPopup>
          <AlertDialogTitle>Overwrite telemetry notes?</AlertDialogTitle>
          <AlertDialogDescription>
            The previous draft is replaced if you continue. You can postpone
            and come back later.
          </AlertDialogDescription>
          <div className="mt-6 flex items-center justify-between gap-2">
            <AlertDialogClose
              render={<Button variant="ghost">Remind me later</Button>}
            />
            <div className="flex gap-2">
              <AlertDialogClose
                render={<Button variant="outline">Cancel</Button>}
              />
              <AlertDialogClose
                render={<Button variant="danger">Overwrite</Button>}
              />
            </div>
          </div>
        </AlertDialogPopup>
      </AlertDialogPortal>
    </AlertDialog>
  )
}

With blurred backdrop

Add backdrop-blur-sm and isolation-auto on the backdrop. The second undoes the default isolate so blur can take effect.

import { BanIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"

export default function Example() {
  return (
    <AlertDialog>
      <AlertDialogTrigger
        render={
          <Button variant="danger">
            <BanIcon />
            Scrub Launch
          </Button>
        }
      />
      <AlertDialogPortal>
        <AlertDialogBackdrop className="backdrop-blur-sm isolation-auto" />
        <AlertDialogPopup>
          <AlertDialogTitle>Scrub the launch?</AlertDialogTitle>
          <AlertDialogDescription>
            The window closes for this orbit. Ground control will need a new
            clearance before the next attempt.
          </AlertDialogDescription>
          <div className="mt-6 flex justify-end gap-2">
            <AlertDialogClose render={<Button variant="outline">Cancel</Button>} />
            <AlertDialogClose render={<Button variant="danger">Scrub</Button>} />
          </div>
        </AlertDialogPopup>
      </AlertDialogPortal>
    </AlertDialog>
  )
}

Detached trigger

A handle from createAlertDialogHandle() connects a trigger to a dialog anywhere else in the tree so neither has to be the other's descendant.

import { OctagonXIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
  createAlertDialogHandle,
} from "@/components/ui/alert-dialog"

// Created once outside the component. Connects the trigger to the
// dialog without either one needing to be a descendant of the other.
const handle = createAlertDialogHandle()

export default function Example() {
  return (
    <>
      <AlertDialogTrigger
        handle={handle}
        render={
          <Button variant="danger">
            <OctagonXIcon />
            Abort Mission
          </Button>
        }
      />
      <AlertDialog handle={handle}>
        <AlertDialogPortal>
          <AlertDialogBackdrop />
          <AlertDialogPopup>
            <AlertDialogTitle>Abort the mission?</AlertDialogTitle>
            <AlertDialogDescription>
              The countdown stops immediately. Ground control will need a
              full resync before the next attempt.
            </AlertDialogDescription>
            <div className="mt-6 flex justify-end gap-2">
              <AlertDialogClose render={<Button variant="outline">Stand Down</Button>} />
              <AlertDialogClose render={<Button variant="danger">Abort</Button>} />
            </div>
          </AlertDialogPopup>
        </AlertDialogPortal>
      </AlertDialog>
    </>
  )
}

Multiple triggers with payload

Several triggers can share one handle and dialog. Each passes its own payload. A function child reads it back so the dialog can render the matching copy.

import {
  DropletsIcon,
  LogOutIcon,
  RocketIcon,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
  AlertDialog,
  AlertDialogBackdrop,
  AlertDialogClose,
  AlertDialogDescription,
  AlertDialogPopup,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
  createAlertDialogHandle,
} from "@/components/ui/alert-dialog"

type ActionPayload = {
  title: string
  description: string
  confirmLabel: string
}

// One handle and one dialog. Each trigger passes its own payload so the
// popup can render the right copy for whichever action was pressed.
const handle = createAlertDialogHandle<ActionPayload>()

const actions = [
  {
    label: "Abort Launch",
    icon: RocketIcon,
    payload: {
      title: "Abort the launch?",
      description:
        "Fuel drains automatically and the pad resets. This can't be undone mid-sequence.",
      confirmLabel: "Abort",
    },
  },
  {
    label: "Purge Fuel Tank",
    icon: DropletsIcon,
    payload: {
      title: "Purge the fuel tank?",
      description: "Every drop vents to atmosphere. Refueling takes another two orbits.",
      confirmLabel: "Purge",
    },
  },
  {
    label: "Sign Out Of Mission Control",
    icon: LogOutIcon,
    payload: {
      title: "Sign out of mission control?",
      description: "Unsaved telemetry notes will be lost the moment you log off.",
      confirmLabel: "Sign Out",
    },
  },
]

export default function Example() {
  return (
    <>
      {actions.map((action) => {
        const Icon = action.icon
        return (
          <AlertDialogTrigger
            key={action.label}
            handle={handle}
            payload={action.payload}
            render={
              <Button variant="outline">
                <Icon />
                {action.label}
              </Button>
            }
          />
        )
      })}
      <AlertDialog<ActionPayload> handle={handle}>
        {({ payload }) => (
          <AlertDialogPortal>
            <AlertDialogBackdrop />
            <AlertDialogPopup>
              <AlertDialogTitle>{payload?.title}</AlertDialogTitle>
              <AlertDialogDescription>{payload?.description}</AlertDialogDescription>
              <div className="mt-6 flex justify-end gap-2">
                <AlertDialogClose render={<Button variant="outline">Cancel</Button>} />
                <AlertDialogClose
                  render={<Button variant="danger">{payload?.confirmLabel}</Button>}
                />
              </div>
            </AlertDialogPopup>
          </AlertDialogPortal>
        )}
      </AlertDialog>
    </>
  )
}

Props

AlertDialog

PropTypeDefault
openboolean
defaultOpenbooleanfalse
onOpenChange(open: boolean, eventDetails) => void
handleAlertDialogHandle<Payload>

AlertDialogTrigger

PropTypeDefault
payloadPayload
idstring
handleAlertDialogHandle<Payload>

AlertDialogPopup

PropTypeDefault
initialFocusboolean | RefObject | (openType) => boolean | HTMLElement | null | void
finalFocusboolean | RefObject | (closeType) => boolean | HTMLElement | null | void

This covers the parts used above. See the Base UI Alert Dialog docs for actionsRef, event details, CSS variables and data attributes.