Skip to contentVibraUI
Feedback & status

Alert dialog

A modal question that has to be answered before the page goes on.

Vibra draws the popup as the kit's elev-2 modal layer over the --overlay scrim instead of 10% black, and has it rise 8px on the emphasized ease rather than zoom, leaving on the fast exit curve; the footer strip is divided by --rule and the title set at text-md. A title given one of the theme's type registers (type-display text-2xl) takes it whole and drops its own face and weight, as DialogTitle does. The header aligns to the inline start where shadcn's aligns left, so a right-to-left dialog reads from the right, and answers that outgrow the footer's row wrap onto a second one instead of running out of the popup. ConfirmDialog is this with the confirm awaited and a typed-word gate.

Install

npx shadcn@latest add @vibra/alert-dialog

Needs the @vibra registry in your components.json — set it up once.

Examples

Small

size="sm": the header centred over its icon, and the two answers side by side.

With an icon

AlertDialogMedia beside the title from the sm breakpoint. A question with no danger in it answers with the default fill.

Destructive, with what goes

The deletion says what it takes with it, in a description that holds the list, so it is read on opening.

Three answers

Discard, keep editing or save. The focus opens on the one answer Enter can't regret.

Gated by a checkbox

The action waits for a precondition, focusable all the while and described by what it waits for.

Long content

The middle scrolls between a header and a footer that stay put, as a named region the keyboard reaches first.

Waits for the action

The action stays busy, holding the focus, and the dialog stays open until the refund goes through.

When the action fails

A danger callout where the reader is looking, and the same button offering to try again.

Opened from a menu

Opened from state, since the menu is gone by then; finalFocus hands the focus back to the menu button.

Opened by the app

An idle session about to end. Staying is the fill, the cancel and the first focus.

Right to left

The popup is portalled out of the page, so it carries dir and lang itself.

Props

PropTypeDefaultDescription
AlertDialogContent.size"default" | "sm""default"sm narrows the dialog and puts the two actions side by side.
AlertDialogContent.initialFocus / finalFocusboolean | React.RefObject<HTMLElement | null> | ((type: InteractionType) => HTMLElement | boolean | null | void)the first control / the triggerWhere the focus lands on opening and returns on closing. Name them when the safe answer is not the first button, or when a menu item opened the dialog.
AlertDialogMediaReact.ComponentProps<"div">—An icon tile: beside the title at the default size from the sm breakpoint, over it otherwise.
AlertDialogCancel.variant / sizeButtonProps["variant"] / ButtonProps["size"]"outline" / "default"The cancel button's look; AlertDialogAction takes every Button prop.

Dependencies

Source

components/ui/alert-dialog.tsx
"use client"

import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
import { cn } from "@/lib/utils"

import { Button } from "@/components/ui/button"

function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
  return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}

function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
  return (
    <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
  )
}

function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
  return (
    <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
  )
}

function AlertDialogOverlay({
  className,
  ...props
}: AlertDialogPrimitive.Backdrop.Props) {
  return (
    <AlertDialogPrimitive.Backdrop
      data-slot="alert-dialog-overlay"
      className={cn(
        "fixed inset-0 isolate z-50 bg-overlay supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-open:duration-(--duration-base) data-open:ease-(--ease-standard) data-closed:animate-out data-closed:fade-out-0 data-closed:duration-(--duration-fast) data-closed:ease-(--ease-exit)",
        className
      )}
      {...props}
    />
  )
}

function AlertDialogContent({
  className,
  size = "default",
  ...props
}: AlertDialogPrimitive.Popup.Props & {
  size?: "default" | "sm"
}) {
  return (
    <AlertDialogPortal>
      <AlertDialogOverlay />
      <AlertDialogPrimitive.Popup
        data-slot="alert-dialog-content"
        data-size={size}
        className={cn(
          "group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 elev-2 p-4 text-popover-foreground outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:slide-in-from-bottom-2 data-open:duration-(--duration-slow) data-open:ease-(--ease-emphasized) data-closed:animate-out data-closed:fade-out-0 data-closed:duration-(--duration-fast) data-closed:ease-(--ease-exit)",
          className
        )}
        {...props}
      />
    </AlertDialogPortal>
  )
}

function AlertDialogHeader({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-dialog-header"
      className={cn(
        "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-start sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
        className
      )}
      {...props}
    />
  )
}

// Answers that outgrow the row wrap onto a second one rather than running out
// of the popup; a dialog with three long answers is better given the room
// (a wider max-w on AlertDialogContent), and this is what happens if not.
function AlertDialogFooter({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-dialog-footer"
      className={cn(
        "-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t border-rule bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:flex-wrap sm:justify-end",
        className
      )}
      {...props}
    />
  )
}

function AlertDialogMedia({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-dialog-media"
      className={cn(
        "mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
        className
      )}
      {...props}
    />
  )
}

/**
 * A class naming one of the theme's type registers — type-display and its kin.
 * A register sets the family, the weight and the tracking in one class, which
 * the class merge cannot see as replacing the title's own type, so the two
 * stayed side by side and the stylesheet's order decided between them.
 */
const TYPE_REGISTER = /(?:^|\s)type-[a-z]/

function AlertDialogTitle({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
  // A title given a register keeps it whole; otherwise the title's own type
  // applies, and any single class still replaces its counterpart.
  const ownType = typeof className === "string" && TYPE_REGISTER.test(className)
  return (
    <AlertDialogPrimitive.Title
      data-slot="alert-dialog-title"
      className={cn(
        !ownType && "font-heading text-md font-medium",
        "sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
        className
      )}
      {...props}
    />
  )
}

function AlertDialogDescription({
  className,
  ...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
  return (
    <AlertDialogPrimitive.Description
      data-slot="alert-dialog-description"
      className={cn(
        "text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
        className
      )}
      {...props}
    />
  )
}

function AlertDialogAction({
  className,
  ...props
}: React.ComponentProps<typeof Button>) {
  return (
    <Button
      data-slot="alert-dialog-action"
      className={cn(className)}
      {...props}
    />
  )
}

function AlertDialogCancel({
  className,
  variant = "outline",
  size = "default",
  ...props
}: AlertDialogPrimitive.Close.Props &
  Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
  return (
    <AlertDialogPrimitive.Close
      data-slot="alert-dialog-cancel"
      className={cn(className)}
      render={<Button variant={variant} size={size} />}
      {...props}
    />
  )
}

export {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogOverlay,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
}