Qure UI
Components

Alert Dialog

A confirmation that cannot be dismissed by accident.

'use client'import {  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,  AlertDialogTitle, AlertDialogTrigger,} from '@/registry/qure/ui/alert-dialog'import { Button } from '@/registry/qure/ui/button'export default function AlertDialogDemo() {  return (    <AlertDialog>      <AlertDialogTrigger render={<Button variant="destructive">Delete study</Button>} />      <AlertDialogContent>        <AlertDialogHeader>          <AlertDialogTitle>Delete CT Thorax, ACC-100482?</AlertDialogTitle>          <AlertDialogDescription>            The study, its 214 images and the unsigned report are removed from the archive.            The audit entry stays.          </AlertDialogDescription>        </AlertDialogHeader>        <AlertDialogFooter>          <AlertDialogCancel render={<Button variant="tertiary">Keep study</Button>} />          <AlertDialogAction render={<Button variant="destructive">Delete</Button>} />        </AlertDialogFooter>      </AlertDialogContent>    </AlertDialog>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/alert-dialog.json

Usage

import {
  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,
  AlertDialogTitle, AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="destructive">Delete study</Button>} />
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete CT Thorax, ACC-100482?</AlertDialogTitle>
      <AlertDialogDescription>The study and its 214 images are removed.</AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel render={<Button variant="tertiary">Keep study</Button>} />
      <AlertDialogAction render={<Button variant="destructive">Delete</Button>} />
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Alert Dialog or Dialog

They look nearly the same on purpose and they behave differently on purpose.

DialogAlert Dialog
Click the backdropclosesnothing
Click outsideclosesnothing
Escapeclosescloses
Close button in the corneryesno

Base UI enforces the difference in the type: modal and disablePointerDismissal are not props of AlertDialog.Root, so pointer dismissal cannot be put back. That is the entire reason to reach for it — a misplaced click beside a dialog should not count as an answer.

Escape does still close an alert dialog. That surprises people who expect it to be sealed, and it is correct: the ARIA authoring practices require Escape on alertdialog, and a modal with no keyboard exit is a trap. Treat Escape as a synonym for the cancel button, and make sure cancelling is always the safe outcome.

Use it when the answer is destructive or irreversible and the user has to say so; use Dialog for everything else, because a dialog that cannot be dismissed is a rude way to ask an ordinary question. A form is not an alert dialog. Nor is a notice with one OK button — that is a toast.

Composition

<AlertDialog>                {/* owns the open state */}
  <AlertDialogTrigger />     {/* renders your Button */}
  <AlertDialogContent>       {/* portal + backdrop + popup */}
    <AlertDialogHeader>      {/* title and description, scrollable */}
      <AlertDialogTitle />        {/* labels the dialog. Required. */}
      <AlertDialogDescription />  {/* describes it. State the consequence. */}
    </AlertDialogHeader>
    <AlertDialogFooter>      {/* the action row, with an optional note */}
      <AlertDialogCancel />  {/* the way out. Always present. */}
      <AlertDialogAction />  {/* the destructive answer. */}
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Title and Description are not decoration: Base UI wires them to the popup's aria-labelledby and aria-describedby, so leaving the description out means a screen-reader user hears the question and not the consequence.

Both Cancel and Action are Close underneath — they close on click. For an action that has to wait on the server, use a plain Button instead and close it yourself; see below.

Delete a study

'use client'import {  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,  AlertDialogTitle, AlertDialogTrigger,} from '@/registry/qure/ui/alert-dialog'import { Button } from '@/registry/qure/ui/button'export default function AlertDialogDemo() {  return (    <AlertDialog>      <AlertDialogTrigger render={<Button variant="destructive">Delete study</Button>} />      <AlertDialogContent>        <AlertDialogHeader>          <AlertDialogTitle>Delete CT Thorax, ACC-100482?</AlertDialogTitle>          <AlertDialogDescription>            The study, its 214 images and the unsigned report are removed from the archive.            The audit entry stays.          </AlertDialogDescription>        </AlertDialogHeader>        <AlertDialogFooter>          <AlertDialogCancel render={<Button variant="tertiary">Keep study</Button>} />          <AlertDialogAction render={<Button variant="destructive">Delete</Button>} />        </AlertDialogFooter>      </AlertDialogContent>    </AlertDialog>  )}

Name the thing in the title. "Are you sure?" is not a question anyone can answer — "Delete CT Thorax, ACC-100482?" is. Say what survives as well as what goes.

With a note

'use client'import {  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,  AlertDialogTitle, AlertDialogTrigger,} from '@/registry/qure/ui/alert-dialog'import { Button } from '@/registry/qure/ui/button'export default function AlertDialogNote() {  return (    <AlertDialog>      <AlertDialogTrigger render={<Button variant="secondary">Retract report</Button>} />      <AlertDialogContent>        <AlertDialogHeader>          <AlertDialogTitle>Retract the signed report?</AlertDialogTitle>          <AlertDialogDescription>            The referring clinician has already received it. Retracting sends them a            notification and reopens the report for editing.          </AlertDialogDescription>        </AlertDialogHeader>        {/* Figma's Action-bar has a note slot on the left of the buttons; this            is what it is for — a consequence too small to be the description            and too important to leave out. */}        <AlertDialogFooter note="Recorded against your account.">          <AlertDialogCancel render={<Button variant="tertiary">Cancel</Button>} />          <AlertDialogAction render={<Button variant="destructive">Retract</Button>} />        </AlertDialogFooter>      </AlertDialogContent>    </AlertDialog>  )}

Figma's Action-bar (node 3334:7269) has a note slot to the left of the buttons; note on the footer fills it. Use it for a consequence too small to be the description and too important to leave out — "recorded against your account", "the referrer is notified".

Focus on the safe option

'use client'import * as React from 'react'import {  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader,  AlertDialogTitle, AlertDialogTrigger,} from '@/registry/qure/ui/alert-dialog'import { Button } from '@/registry/qure/ui/button'export default function AlertDialogFocus() {  const cancelRef = React.useRef<HTMLButtonElement>(null)  return (    <AlertDialog>      <AlertDialogTrigger render={<Button variant="tertiary">Discard draft</Button>} />      <AlertDialogContent initialFocus={cancelRef}>        <AlertDialogHeader>          <AlertDialogTitle>Discard this draft report?</AlertDialogTitle>          <AlertDialogDescription>            Six minutes of dictation on ACC-100479 have not been saved. Discarding cannot be            undone.          </AlertDialogDescription>        </AlertDialogHeader>        <AlertDialogFooter>          {/* Focus starts on the safe option. Base UI would otherwise focus              the first tabbable element, which is right here by luck rather              than by design — say it, and it stays true if the order changes. */}          <AlertDialogCancel ref={cancelRef} render={<Button variant="tertiary">Keep editing</Button>} />          <AlertDialogAction render={<Button variant="destructive">Discard</Button>} />        </AlertDialogFooter>      </AlertDialogContent>    </AlertDialog>  )}

initialFocus takes a ref. Base UI would otherwise focus the first tabbable element, which is the cancel button here by luck rather than by design; saying it keeps it true if the order changes. A destructive dialog where Enter fires the destructive button is a trap for anyone who opened it by keyboard.

Controlled, with a pending action

'use client'import * as React from 'react'import {  AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription,  AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,} from '@/registry/qure/ui/alert-dialog'import { Button } from '@/registry/qure/ui/button'export default function AlertDialogControlled() {  const [open, setOpen] = React.useState(false)  const [pending, setPending] = React.useState(false)  const [purged, setPurged] = React.useState(0)  async function purge() {    setPending(true)    await new Promise(resolve => setTimeout(resolve, 900))    setPending(false)    setPurged(n => n + 12)    setOpen(false)  }  return (    <div className="flex items-center gap-3">      <Button variant="destructive" onClick={() => setOpen(true)}>Purge expired studies</Button>      {purged > 0 ? <span className="text-muted-foreground text-sm">{purged} purged</span> : null}      <AlertDialog open={open} onOpenChange={setOpen}>        <AlertDialogContent>          <AlertDialogHeader>            <AlertDialogTitle>Purge 12 expired studies?</AlertDialogTitle>            <AlertDialogDescription>              These studies passed their retention date on 31 March 2025. Purging removes the              images; the reports and audit entries are kept.            </AlertDialogDescription>          </AlertDialogHeader>          <AlertDialogFooter>            <AlertDialogCancel render={<Button variant="tertiary" disabled={pending}>Cancel</Button>} />            {/* Not AlertDialogAction: this must not close until the request                comes back, and Close closes on click. */}            <Button variant="destructive" disabled={pending} onClick={purge}>              {pending ? 'Purging…' : 'Purge'}            </Button>          </AlertDialogFooter>        </AlertDialogContent>      </AlertDialog>    </div>  )}

Drive it with open and onOpenChange when the trigger lives somewhere else — a menu item, a row action, a keyboard shortcut. The confirm button here is a plain Button, not AlertDialogAction, because Action closes on click and this one must stay open until the request returns.

Under prefers-reduced-motion the transitions are dropped, not shortened. A confirmation is not the place to make someone wait out an animation they asked not to see.

API Reference

Prop

Type

The rest is Base UI's Alert Dialog — minus modal and disablePointerDismissal, which it does not have.

On this page