Alert Dialog React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React alert dialog for confirmations: an async confirm that loads, fails inline and shakes, plus an await confirm() API.
Press the trigger and the page dims while the card rises into the center on a smooth spring, its icon popping in a beat later. Focus lands on Cancel, the safe choice; Tab moves between the two buttons, Enter presses the focused one and Escape cancels. Clicking the dimmed page does nothing, because the question has to be answered. When the confirm returns a promise the action shows a spinner, Cancel and Escape wait, and the dialog closes once the request succeeds; if it fails the card shakes, the error slides in above the buttons and the action is ready to retry. In a type-to-confirm field Enter confirms as soon as the action is enabled. When it closes, focus goes back to the trigger, or to whatever had it before a confirm() call or a trigger-less open. On a phone it stays a centered card with the buttons stacked full width, the action on top and 44px tall. With reduced motion it only fades.
Alert Dialog is a Pro React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add alert-dialog, together with Button, Motion presets, Tones, useControllableState and UIProvider and the npm packages @radix-ui/react-alert-dialog, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add alert-dialogAlert Dialog is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import AlertDialog from @/components/ui/alert-dialog.
import type { ReactNode } from "react"
import { Trash2 } from "lucide-react"
import { AlertDialog, AlertDialogProvider, confirm } from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
async function deleteListing(id: string) {
const res = await fetch(`/api/listings/${id}`, { method: "DELETE" })
if (!res.ok) throw new Error("Could not delete the listing. Try again.")
}
export function DeleteListing({ id }: { id: string }) {
return (
<AlertDialog
trigger={
<Button variant="soft" tone="danger" leftIcon={<Trash2 />}>
Delete
</Button>
}
tone="danger"
title="Delete this listing?"
description="The listing “Linus city bike” and its 12 messages will be deleted for good."
confirmLabel="Delete"
cancelLabel="Cancel"
confirmLoadingText="Deleting"
onConfirm={() => deleteListing(id)}
/>
)
}
// the imperative api: mount the provider once, then await an answer anywhere
export function App({ children }: { children: ReactNode }) {
return (
<>
{children}
<AlertDialogProvider defaults={{ cancelLabel: "Cancel" }} />
</>
)
}
export async function signOutEverywhere() {
const ok = await confirm({
title: "Sign out of every device?",
description: "You stay signed in on this phone only.",
confirmLabel: "Sign out",
})
if (ok) console.log("signed out")
}Props
titledefault""ReactNodeThe question, phrased so the buttons answer it. Without one the dialog asks "Are you sure?", so a screen reader always has a title.
descriptiondefault""ReactNodeThe consequence, in a sentence or two: what will be lost and whether it can be undone.
confirmLabeldefault"Continue"stringThe action. Name what it does ("Delete listing"), not "Yes" or "OK".
cancelLabeldefault"Cancel"stringThe safe way out. It gets the focus when the dialog opens.
confirmLoadingTextdefault""stringNext to the spinner while onConfirm runs, like "Deleting". The button reserves room for it, so it never changes width.
errorTextdefault""ReactNodeShown above the buttons when onConfirm rejects or throws. When empty, the error's own message is used if it has one, then "Something went wrong. Try again."
hideTitledefaultfalsebooleanKeeps the title for screen readers only, for a dialog whose description says it all.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Colors the action and the icon tile. danger for anything destructive (with a trash icon), warning for unsaved changes, success for a positive step; neutral is black in light mode and white in dark mode.
colordefault""stringAny CSS color for the action and the icon tile, replacing the tone. Text on the button turns black or white by lightness.
sizedefault"sm""sm" | "md"Max width 400px (sm) or 480px (md) for longer text. On a phone it is always 16px clear of both edges.
radiusdefault"lg""none" | "sm" | "md" | "lg" | "full"Corner shape of the card: 0, 12, 16 or 24px. The icon tile and the buttons follow along, and full turns them into a circle and pills.
aligndefault"start""start" | "center"start reads like a desktop dialog with the buttons on the right; center stacks icon and text in the middle and splits the buttons evenly, like an iOS alert.
defaultOpendefaultfalsebooleanStarts open while uncontrolled. The playground reopens it on change so every setting shows.
confirmDisableddefaultfalsebooleanBlocks the action, e.g. until the listing's name is typed. Enter in a text field confirms once it is enabled.
openbooleanControlled open state; pair it with onOpenChange.
onOpenChange(open: boolean) => voidCalled when it opens or closes, whether by the trigger, Cancel, Escape or a finished confirm.
triggerReactNodeThe element that opens it, rendered with asChild: a single element that takes a ref, usually a Button.
iconReactNodeShown in a 40px tile above the title. Defaults to a trash can for danger, a triangle for warning and a check for success, and none for the rest; null hides it.
childrenReactNodeExtra content between the text and the buttons, like a type-to-confirm field. It scrolls when the screen is short.
onConfirm() => (void | Promise<unknown>)Runs on the action. Return a promise to keep the dialog open with the action loading: resolving closes it, rejecting (or throwing) shows the error and shakes the card. A late result after the dialog closed is ignored.
onCancel() => voidCalled on Cancel or Escape, not when the dialog closes after a confirm.
containerHTMLElement | nullWhere the portal mounts. document.body by default.
classNamesPartial<Record<"overlay" | "content" | "icon" | "header" | "title" | "description" | "body" | "error" | "footer" | "cancel" | "action", string>>Extra classes for the inner parts.
classNamestringExtra classes for the card, merged last. The rest of the props (id, aria-*, onOpenAutoFocus...) go to it too.
Parts
confirm
functionAsks from anywhere, even outside React: resolves true once confirmed (after onConfirm settled) and false when cancelled. Takes every AlertDialog prop except trigger and the open state. Calls made while one is open wait their turn, and focus returns to what had it when confirm() was called. Needs <AlertDialogProvider /> mounted; without it, development warns and the browser's confirm steps in. Signature: (options: ConfirmOptions) => Promise<boolean>.
useConfirm
hookThe same confirm function, for code that prefers a hook. Signature: () => typeof confirm.
AlertDialogProvider
componentMount once near the root, next to the toaster. Renders confirm() calls one at a time; defaults go under every call's options (translated labels, a house tone). If the last provider unmounts, calls still waiting resolve false. Props: { defaults?: ConfirmOptions }.
AlertDialogRoot
componentThe parts API, for custom layouts: AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogBody, AlertDialogError, AlertDialogFooter, AlertDialogCancel and AlertDialogAction. Props: { open?; defaultOpen?; onOpenChange?; onCancel?; children }.
AlertDialogTrigger
componentOpens it. Use asChild with a Button; focus returns here when the dialog closes. Props: Radix AlertDialog.Trigger props.
AlertDialogContent
componentThe overlay and the card. Takes size, tone, color, radius, align, container and classNames { overlay, content }, plus Radix Content props (onOpenAutoFocus, onCloseAutoFocus...). Props: AlertDialogContentProps.
AlertDialogHeader
componentTitle and description, with an optional icon tile above them in the content's tone. Scrolls on its own when the text outgrows a short screen. Props: { icon?: ReactNode; classNames?: { icon }; children } & ComponentProps<"div">.
AlertDialogIcon
componentThe 40px tile in the tone's soft color that pops in on the bouncy spring. The header renders it for you when you pass icon. Takes span props.
AlertDialogTitle
componentThe question, 18px semibold. Required by screen readers; hide it with the sr-only class if the design has none. Props: Radix AlertDialog.Title props.
AlertDialogDescription
componentThe consequence in muted text. Leave it out and pass aria-describedby={undefined} to the content. Props: Radix AlertDialog.Description props.
AlertDialogBody
componentExtra content between the text and the buttons (a type-to-confirm field). Scrolls when the screen is short. Takes div props.
AlertDialogAction
componentThe confirm Button in the content's tone. Runs onConfirm with the loading, error and shake behavior; call event.preventDefault() in onClick to keep it from running. Props: ButtonProps & { onConfirm?; errorText? }.
AlertDialogCancel
componentThe soft Cancel Button. Focused on open, disabled while a confirm runs. Props: ButtonProps.
AlertDialogError
componentShows the failed confirm's message with role alert; children replace it. Takes p props.
ConfirmOptions
typeWhat confirm() takes: the AlertDialog props without trigger, open, defaultOpen and onOpenChange.
Examples
Async delete that can fail
A promise keeps it open and loading; a rejection shakes the card, shows the message and lets you retry.
Imperative confirm()
Mount AlertDialogProvider once, then await an answer from any handler. No dialog markup at the call site.
Type to confirm
confirmDisabled until the word matches, in any case. Enter in the field confirms.
Brand tone for a paid step
Non-destructive, with a custom icon and the price in the question and on the button.
Unsaved changes
warning brings its own icon; size md gives two long labels room to sit side by side.
Queued confirms
Two confirm() calls at once show one after the other, each resolving on its own.
Centered, like iOS
align="center" with radius="full": a round icon tile and two pills that share the width.
Dependencies
Files
- components/ui/alert-dialog.tsx
FAQ
Is the React Alert Dialog component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Alert Dialog?
Run npx wingo-ui@latest add alert-dialog. It also adds the Wingo UI items it builds on (Button, Motion presets, Tones, useControllableState and UIProvider) and the npm packages @radix-ui/react-alert-dialog, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/alert-dialog.
How does Alert Dialog behave on phones?
On a phone it stays a centered card with the buttons stacked full width, the action on top and 44px tall.
See also
Used in
More React overlay components
Articles using this component
- API Key Management UI: Create, Show Once, Roll, Revoke
Build it: screens and apps10 min read
- React Action Sheet: iOS-Style, Async and Responsive
Mobile UI in React9 min read
- React Settings Page: Section Nav, Save Bar, Danger Zone
Build it: screens and apps11 min read
- React Slide to Confirm: When It Beats a Confirm Dialog
Mobile UI in React8 min read