Alert Dialog

Installation

bash
npx @smicolon/fasla-ui@latest add alert-dialog

Run 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 alertdialog role. 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

PropTypeDefaultDescription
openboolean—Shows or hides the dialog, when you control it.
defaultOpenbooleanfalseShows 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.
actionsRefRefObject—A ref with close(), to close the dialog from code.
handleAlertDialogHandle—Connects the dialog to triggers elsewhere on the page. Create one with AlertDialog.createHandle() from @base-ui/react/alert-dialog.
triggerIdstring | null—Which trigger the dialog belongs to, when several share it and you control open.
defaultTriggerIdstring | null—Which trigger the dialog belongs to on first render, with defaultOpen.
childrenReactNode—The trigger and the content.

AlertDialogTrigger

PropTypeDefaultDescription
renderReactElement—Renders your own element in place of the plain button, such as a Button.
handleAlertDialogHandle—The handle of a dialog elsewhere that this trigger opens.
payloadunknown—Data handed to the dialog when this trigger opens it.
disabledbooleanfalseStops this trigger opening the dialog.
childrenReactNode—The trigger's label.
classNamestring—Extra classes for the trigger.

AlertDialogContent

PropTypeDefaultDescription
align"edge" | "center""edge"edge starts the text at the beginning of the line and puts the actions at the end. center centres both.
destructivebooleanfalseTurns the main action into the destructive Button.
iconboolean | ReactNodefalsetrue for the alert icon, or any icon element. Either way it is 24px, in the title's colour.
containerHTMLElement—Where the dialog renders. Defaults to body.
initialFocusboolean | RefObject—The element focused when the dialog opens. Defaults to the first control.
finalFocusboolean | RefObject—The element focused when the dialog closes. Defaults to the trigger.
childrenReactNode—The header, any additional content and the footer.
classNamestring—Extra classes for the dialog box.

AlertDialogHeader

PropTypeDefaultDescription
childrenReactNode—The title and the description.
classNamestring—Extra classes for the header.

AlertDialogTitle

PropTypeDefaultDescription
childrenReactNode—The question the dialog asks.
classNamestring—Extra classes for the title.

AlertDialogDescription

PropTypeDefaultDescription
childrenReactNode—What happens if the user goes ahead.
classNamestring—Extra classes for the description.

AlertDialogFooter

PropTypeDefaultDescription
childrenReactNode—Cancel, then the main action.
classNamestring—Extra classes for the row of actions.

AlertDialogCancel

PropTypeDefaultDescription
onClick(event) => void—Runs before the dialog closes.
disabledbooleanfalseDisables the button.
childrenReactNode—The label, usually Cancel.
classNamestring—Extra classes for the button.

AlertDialogAction

PropTypeDefaultDescription
onClick(event) => void—Runs the action, then closes the dialog. Call event.preventBaseUIHandler() to keep it open.
loadingbooleanfalseShows a spinner, disables the button and announces loadingLabel.
loadingLabelstring"Loading"Screen-reader text announced while loading.
disabledbooleanfalseDisables the button.
childrenReactNode—The action's label.
classNamestring—Extra classes for the button.

Usage

Confirm a delete

tsx
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

tsx
"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

tsx
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

tsx
"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>
  )
}