Alert Dialog
Installation
npx @smicolon/fasla-ui@latest add alert-dialogRun init once per project before adding components. See Installation.
Alert Dialog is built on Base UI, so it needs the @base-ui/react package, and its actions use button. add installs both with the component, and so does the shadcn CLI.
Preview
Press the button to open the dialog. Tab moves between its buttons, and Escape closes it.
Alignment
align sets the layout. edge, the default, starts the title and description at the beginning of the line and puts the actions at the end of the row. center centres the text and the actions. Keep center for a short description: centred lines are harder to read once the text runs to several.
Destructive
Set destructive when the action can't be undone, such as deleting a project. The main action becomes the destructive Button, and Cancel stays as it is. Name the action in its label, such as Delete, so the button says what happens on its own.
Icon
icon adds an alert icon to the title, 24px and in the title's colour. With edge it sits beside the title, centred on its first line, and the description and any additional content line up with the title, not the icon. With center it sits above the title. To show another icon, pass any icon element in place of true, such as a sign-out icon below. It takes the same size, colour and place.
Additional content
Anything you place between AlertDialogHeader and AlertDialogFooter sits between the text and the actions, 16px from each, as wide as the text. Use it for one short field or a checkbox the decision depends on, such as typing the project's name before deleting it. A longer form belongs in a page or a regular dialog.
Waiting for the action
The main action closes the dialog after its onClick runs. When the action takes time, call event.preventBaseUIHandler() in onClick to keep the dialog open, set loading on the action while the request runs, and close the dialog through open once it finishes.
Long text
A long title or description wraps inside the 512px box. When the dialog is taller than the window, it scrolls inside, and on a phone it keeps 16px from each edge of the screen.
Accessibility
- The dialog has the
alertdialogrole. Its title names it and its description describes it, so a screen reader reads both when it opens. - The icon, built in or your own, is hidden from screen readers. Make the title name the action on its own, such as Sign out of every device.
- When the dialog opens, focus moves to its first control: Cancel, or the first field of any additional content. Tab and Shift+Tab stay inside the dialog, and focus returns to the trigger when it closes.
- Escape closes the dialog, as Cancel does. A press on the overlay does nothing, so a stray click never makes the decision.
- While the dialog is open, the page behind it can't be reached with the keyboard or the pointer, and it doesn't scroll.
- The dialog takes the page's direction, language and font, though it renders at the end of
body. In Arabic the whole layout mirrors and Cancel still comes first in reading order. Direction needs no prop.
Props
AlertDialog
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Shows or hides the dialog, when you control it. |
defaultOpen | boolean | false | Shows the dialog on first render. |
onOpenChange | (open, details) => void | — | Called when the dialog opens or closes. |
onOpenChangeComplete | (open) => void | — | Called once the dialog has finished opening or closing. |
actionsRef | RefObject | — | A ref with close(), to close the dialog from code. |
handle | AlertDialogHandle | — | Connects the dialog to triggers elsewhere on the page. Create one with AlertDialog.createHandle() from @base-ui/react/alert-dialog. |
triggerId | string | null | — | Which trigger the dialog belongs to, when several share it and you control open. |
defaultTriggerId | string | null | — | Which trigger the dialog belongs to on first render, with defaultOpen. |
children | ReactNode | — | The trigger and the content. |
AlertDialogTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
render | ReactElement | — | Renders your own element in place of the plain button, such as a Button. |
handle | AlertDialogHandle | — | The handle of a dialog elsewhere that this trigger opens. |
payload | unknown | — | Data handed to the dialog when this trigger opens it. |
disabled | boolean | false | Stops this trigger opening the dialog. |
children | ReactNode | — | The trigger's label. |
className | string | — | Extra classes for the trigger. |
AlertDialogContent
| Prop | Type | Default | Description |
|---|---|---|---|
align | "edge" | "center" | "edge" | edge starts the text at the beginning of the line and puts the actions at the end. center centres both. |
destructive | boolean | false | Turns the main action into the destructive Button. |
icon | boolean | ReactNode | false | true for the alert icon, or any icon element. Either way it is 24px, in the title's colour. |
container | HTMLElement | — | Where the dialog renders. Defaults to body. |
initialFocus | boolean | RefObject | — | The element focused when the dialog opens. Defaults to the first control. |
finalFocus | boolean | RefObject | — | The element focused when the dialog closes. Defaults to the trigger. |
children | ReactNode | — | The header, any additional content and the footer. |
className | string | — | Extra classes for the dialog box. |
AlertDialogHeader
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The title and the description. |
className | string | — | Extra classes for the header. |
AlertDialogTitle
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The question the dialog asks. |
className | string | — | Extra classes for the title. |
AlertDialogDescription
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | What happens if the user goes ahead. |
className | string | — | Extra classes for the description. |
AlertDialogFooter
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Cancel, then the main action. |
className | string | — | Extra classes for the row of actions. |
AlertDialogCancel
| Prop | Type | Default | Description |
|---|---|---|---|
onClick | (event) => void | — | Runs before the dialog closes. |
disabled | boolean | false | Disables the button. |
children | ReactNode | — | The label, usually Cancel. |
className | string | — | Extra classes for the button. |
AlertDialogAction
| Prop | Type | Default | Description |
|---|---|---|---|
onClick | (event) => void | — | Runs the action, then closes the dialog. Call event.preventBaseUIHandler() to keep it open. |
loading | boolean | false | Shows a spinner, disables the button and announces loadingLabel. |
loadingLabel | string | "Loading" | Screen-reader text announced while loading. |
disabled | boolean | false | Disables the button. |
children | ReactNode | — | The action's label. |
className | string | — | Extra classes for the button. |
Usage
Confirm a delete
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function DeleteProject({ onDelete }: { onDelete: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" tone="destructive" />}>
Delete project
</AlertDialogTrigger>
<AlertDialogContent destructive>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
The project and all of its files are deleted for everyone on your team. You can't get them back.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onDelete}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Ask for the project name
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
export function DeleteProjectByName({ onDelete }: { onDelete: () => void }) {
const [name, setName] = React.useState("")
return (
<AlertDialog onOpenChange={(open) => open && setName("")}>
<AlertDialogTrigger render={<Button variant="outline" tone="destructive" />}>
Delete project
</AlertDialogTrigger>
<AlertDialogContent destructive icon>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
The project and all of its files are deleted for everyone on your team. You can't get them back.
</AlertDialogDescription>
</AlertDialogHeader>
<Input
label="Type the project name to confirm"
placeholder="Orbit"
value={name}
onChange={(event) => setName(event.target.value)}
/>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction disabled={name !== "Orbit"} onClick={onDelete}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Use your own icon
import { LogOutIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function SignOutEverywhere({ onSignOut }: { onSignOut: () => void }) {
return (
<AlertDialog>
<AlertDialogTrigger render={<Button variant="outline" />}>Sign out</AlertDialogTrigger>
<AlertDialogContent icon={<LogOutIcon strokeWidth={1.5} className="[&:dir(rtl)]:-scale-x-100" />}>
<AlertDialogHeader>
<AlertDialogTitle>Sign out of every device?</AlertDialogTitle>
<AlertDialogDescription>
You'll need to sign in again on your phone, your laptop and any other device you use.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onSignOut}>Sign out</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}Keep it open while the action runs
"use client"
import * as React from "react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
export function DeleteProjectAndWait({ onDelete }: { onDelete: () => Promise<void> }) {
const [open, setOpen] = React.useState(false)
const [pending, setPending] = React.useState(false)
return (
<AlertDialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
<AlertDialogTrigger render={<Button variant="outline" tone="destructive" />}>
Delete project
</AlertDialogTrigger>
<AlertDialogContent destructive>
<AlertDialogHeader>
<AlertDialogTitle>Delete this project?</AlertDialogTitle>
<AlertDialogDescription>
The project and all of its files are deleted for everyone on your team. You can't get them back.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
<AlertDialogAction
loading={pending}
loadingLabel="Deleting"
onClick={async (event) => {
event.preventBaseUIHandler()
setPending(true)
try {
await onDelete()
setOpen(false)
} finally {
setPending(false)
}
}}
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}