HextaUI

Alert dialog

A confirmation dialog for destructive or important actions that waits for async work, and becomes a bottom sheet on phones.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogDemo() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Show dialog
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Publish changes?</AlertDialogTitle>
          <AlertDialogDescription>
            Your changes will be visible to everyone with the link.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Publish</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/alert-dialog.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="outline" />}>
    Delete
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete this project?</AlertDialogTitle>
      <AlertDialogDescription>This can’t be undone.</AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
AlertDialog
├── AlertDialogTrigger
└── AlertDialogContent
    ├── AlertDialogHeader
    │   ├── AlertDialogMedia
    │   ├── AlertDialogTitle
    │   └── AlertDialogDescription
    └── AlertDialogFooter
        ├── AlertDialogCancel
        └── AlertDialogAction

Async action

Return a promise from the onClick of <AlertDialogAction />. The button shows a spinner and the dialog can’t be closed until the promise settles. It closes when it resolves and stays open when it rejects, so people can try again.

"use client"

import * as React from "react"
import { IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

function wait(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

export function AlertDialogAsyncAction() {
  const [shouldFail, setShouldFail] = React.useState(false)
  const [attempts, setAttempts] = React.useState(0)

  return (
    <div className="flex flex-wrap items-center gap-2">
      <AlertDialog onOpenChange={(open) => open && setAttempts(0)}>
        <AlertDialogTrigger render={<Button variant="destructive" />}>
          Delete project
        </AlertDialogTrigger>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <IconTrash />
            </AlertDialogMedia>
            <AlertDialogTitle>Delete “Acme website”?</AlertDialogTitle>
            <AlertDialogDescription>
              {attempts > 0
                ? `Deleting failed ${attempts} time${attempts > 1 ? "s" : ""}. Try again.`
                : "All deployments, domains and environment variables will be removed. This can’t be undone."}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              variant="destructive"
              onClick={async () => {
                await wait(1500)
                if (shouldFail) {
                  setAttempts((count) => count + 1)
                  throw new Error("Request failed")
                }
              }}
            >
              Delete project
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <Button
        variant="ghost"
        size="sm"
        aria-pressed={shouldFail}
        onClick={() => setShouldFail(!shouldFail)}
      >
        Request will {shouldFail ? "fail" : "succeed"}
      </Button>
    </div>
  )
}

Media

<AlertDialogMedia /> places an icon beside the title on desktop and above it on phones. Use variant="destructive" for destructive actions.

import { IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogWithMedia() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Delete file
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia variant="destructive">
            <IconTrash />
          </AlertDialogMedia>
          <AlertDialogTitle>Delete “invoice-2026.pdf”?</AlertDialogTitle>
          <AlertDialogDescription>
            The file moves to the trash and is removed after 30 days.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Delete</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Small

size="sm" centers the content and lays the buttons out side by side, for short questions.

import { IconLogout } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogSmall() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Sign out
      </AlertDialogTrigger>
      <AlertDialogContent size="sm">
        <AlertDialogHeader>
          <AlertDialogMedia>
            <IconLogout />
          </AlertDialogMedia>
          <AlertDialogTitle>Sign out?</AlertDialogTitle>
          <AlertDialogDescription>
            You’ll need to sign in again on this device.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Sign out</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Detached triggers

Create a handle with createAlertDialogHandle() to share one dialog between many triggers. Each trigger passes its own payload, which the dialog reads through a render function.

"use client"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
  createAlertDialogHandle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

const removeMember = createAlertDialogHandle<{ name: string }>()

export function AlertDialogDetachedTriggers() {
  return (
    <div className="flex flex-wrap gap-2">
      {["Olivia", "Liam"].map((name) => (
        <AlertDialogTrigger
          key={name}
          handle={removeMember}
          payload={{ name }}
          render={<Button variant="outline" />}
        >
          Remove {name}
        </AlertDialogTrigger>
      ))}
      <AlertDialog handle={removeMember}>
        {({ payload }) => (
          <AlertDialogContent size="sm">
            <AlertDialogHeader>
              <AlertDialogTitle>Remove {payload?.name}?</AlertDialogTitle>
              <AlertDialogDescription>
                They’ll lose access to this workspace.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction variant="destructive">
                Remove
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        )}
      </AlertDialog>
    </div>
  )
}

Controlled

Pass open and onOpenChange to open it from code, without a trigger. Focus still lands on Cancel.

"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 function AlertDialogControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <>
      <Button
        variant="outline"
        onClick={() => window.setTimeout(() => setOpen(true), 600)}
      >
        Open in 600ms
      </Button>
      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Session expiring</AlertDialogTitle>
            <AlertDialogDescription>
              You’ve been inactive for a while. Stay signed in?
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Sign out</AlertDialogCancel>
            <AlertDialogAction>Stay signed in</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}

Nested

A dialog opened from inside another stacks on top. The parent scales back while the child is open and comes forward again when it closes.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogNested() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Reset settings
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Reset all settings?</AlertDialogTitle>
          <AlertDialogDescription>
            Theme, notifications and shortcuts go back to their defaults.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialog>
            <AlertDialogTrigger render={<Button variant="destructive" />}>
              Reset
            </AlertDialogTrigger>
            <AlertDialogContent size="sm">
              <AlertDialogHeader>
                <AlertDialogTitle>Are you really sure?</AlertDialogTitle>
                <AlertDialogDescription>
                  The parent dialog steps back while this one is open.
                </AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel>Go back</AlertDialogCancel>
                <AlertDialogAction variant="destructive">
                  Reset
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialog>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Long content

Content taller than the screen scrolls inside the dialog while the page stays locked.

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

const sections = Array.from({ length: 12 }, (_, index) => index + 1)

export function AlertDialogLongContent() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Review terms
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Accept the updated terms?</AlertDialogTitle>
          <AlertDialogDescription render={<div />}>
            {sections.map((section) => (
              <p key={section}>
                Section {section}. We may update these terms from time to time.
                Continued use of the service after changes take effect means you
                accept the new terms. Read the <a href="#">full policy</a> for
                details.
              </p>
            ))}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Decline</AlertDialogCancel>
          <AlertDialogAction>Accept</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Right to left

Pass dir to the content as well, because it renders in a portal outside your RTL container. Arrow keys in the footer follow the reading direction.

import { IconTrash } from "@tabler/icons-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export function AlertDialogRtl() {
  return (
    <div dir="rtl">
      <AlertDialog>
        <AlertDialogTrigger render={<Button variant="outline" />}>
          حذف الملف
        </AlertDialogTrigger>
        <AlertDialogContent dir="rtl">
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <IconTrash />
            </AlertDialogMedia>
            <AlertDialogTitle>حذف هذا الملف؟</AlertDialogTitle>
            <AlertDialogDescription>
              لا يمكن التراجع عن هذا الإجراء.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>إلغاء</AlertDialogCancel>
            <AlertDialogAction variant="destructive">حذف</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}
KeyAction
EnterSpaceOn the trigger, opens the dialog and moves focus to Cancel.
TabShift+TabMoves focus between controls. Focus stays inside.
←→↑↓Moves between the footer buttons and wraps around. Left and right follow the reading direction.
EscCloses the dialog and returns focus to the trigger. Ignored while an async action is running.
  • The content has role="alertdialog" and is labelled by its title and described by its description.
  • Focus starts on <AlertDialogCancel />, the least destructive choice. Pass initialFocus to change it.
  • Clicking the backdrop doesn’t close it, so a decision is never dismissed by accident. Set disablePointerDismissal={false} to allow it.
  • On phones it becomes a bottom sheet that can be swiped down to cancel. While an action is pending, swiping, Esc and Cancel are blocked and the buttons stay focusable.
  • With reduced motion on, it fades instead of scaling.

Built on the Base UI drawer, through <Sheet />. Every part accepts the props of the primitive or element it wraps.

PropTypeDefault
open
boolean–
defaultOpen
booleanfalse
onOpenChangeNot called for closes blocked by a pending action.
(open: boolean, details) => void–
onOpenChangeCompleteCalled after the open or close animation.
(open: boolean) => void–
disablePointerDismissalKeeps the dialog open on backdrop clicks.
booleantrue
handleConnects detached triggers.
AlertDialogHandle<Payload>–
actionsRefClose or unmount the dialog imperatively.
RefObject<{ close, unmount }>–
children
ReactNode | ({ payload }) => ReactNode–
PropTypeDefault
handle
AlertDialogHandle<Payload>–
payloadPassed to the dialog’s render function.
Payload–
render
ReactElement | (props, state) => ReactElement<button>
AttributeDescription
data-slot="alert-dialog-trigger"Target triggers in CSS.
data-popup-openPresent while its dialog is open.
PropTypeDefault
size
"default" | "sm""default"
initialFocus
boolean | RefObject | (openType) => HTMLElement | booleanCancel button
finalFocus
boolean | RefObject | (closeType) => HTMLElement | booleanThe trigger
dirSet it when the dialog should be right to left.
"ltr" | "rtl"–
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="alert-dialog-content"Target the dialog in CSS.
data-sizeThe current size.
data-openPresent while open.
data-starting-stylePresent while the dialog animates in.
data-ending-stylePresent while the dialog animates out.
data-nested-drawer-openPresent while a nested dialog is open on top.
data-swipingPresent while it is being swiped on a phone.
--nested-drawersHow many dialogs are open on top of this one.

A <div> that stacks the media, title and description.

AttributeDescription
data-slot="alert-dialog-header"Target the header in CSS.
PropTypeDefault
variant
"default" | "destructive""default"
AttributeDescription
data-slot="alert-dialog-media"Target the media in CSS.
PropTypeDefault
render
ReactElement | (props, state) => ReactElement<h2>
AttributeDescription
data-slot="alert-dialog-title"Target the title in CSS.
PropTypeDefault
renderUse render={<div />} when it holds several paragraphs.
ReactElement | (props, state) => ReactElement<p>
AttributeDescription
data-slot="alert-dialog-description"Target the description in CSS.

A <div> for the buttons. Buttons stack full width on phones, and arrow keys move between them.

AttributeDescription
data-slot="alert-dialog-footer"Target the footer in CSS.

A <Button /> that closes the dialog when clicked. It accepts every Button prop.

PropTypeDefault
onClickReturn a promise to show a spinner and keep the dialog open until it settles. Call event.preventDefault() to keep it open.
(event) => void | PromiseLike<unknown>–
variant
ButtonVariant"default"
AttributeDescription
data-slot="alert-dialog-action"Target actions in CSS.
PropTypeDefault
variant
ButtonVariant"outline"
size
ButtonSize–
disabledAlso disabled while an action is pending.
boolean–
AttributeDescription
data-slot="alert-dialog-cancel"Target the cancel button in CSS. It receives initial focus.
data-disabledPresent while disabled.

<AlertDialogContent /> already renders both. Use them only when composing a custom popup.

PropTypeDefault
keepMountedOn the portal, keeps the dialog in the DOM while closed.
booleanfalse

Returns a handle that connects <AlertDialogTrigger /> elements anywhere on the page to one <AlertDialog />. Type the payload with a generic: createAlertDialogHandle<{ name: string }>().