Alert Dialog

A modal dialog that interrupts the user with important content and expects a response.

"use client"

import * as React from "react"
import { cn } from "cn"
import {
  Dialog as AlertDialogPrimitive,
  DialogTrigger as AlertDialogTriggerPrimitive,
  Heading,
  ModalOverlay as ModalOverlayPrimitive,
  Modal as ModalPrimitive,
  type DialogTriggerProps as AlertDialogTriggerPrimitiveProps,
  type ModalOverlayProps as ModalOverlayPrimitiveProps,
} from "@animate-ui/react/primitives/react-aria"

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

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

function AlertDialogOverlay({
  className,
  children,
  ...props
}: Omit<ModalOverlayPrimitiveProps, "className" | "children"> & {
  className?: string
  children: React.ReactNode
}) {
  return (
    <ModalOverlayPrimitive
      data-slot="alert-dialog-overlay"
      className={cn(
        "bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50",
        className
      )}
      {...props}
    >
      {children}
    </ModalOverlayPrimitive>
  )
}

function AlertDialog({
  className,
  size = "default",
  children,
  from,
  ...props
}: Omit<ModalOverlayPrimitiveProps, "className" | "children"> &
  Pick<React.ComponentProps<typeof ModalPrimitive>, "isDismissable" | "from"> & {
    className?: string
    size?: "default" | "sm"
    children: React.ReactNode
  }) {
  return (
    <AlertDialogOverlay {...props}>
      <ModalPrimitive
        data-slot="alert-dialog-content"
        data-size={size}
        from={from}
        className={cn(
          "bg-popover text-popover-foreground ring-foreground/10 gap-4 rounded-xl p-4 ring-1 data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 outline-none",
          className
        )}
      >
        <AlertDialogPrimitive
          data-slot="alert-dialog"
          role="alertdialog"
          className="[display:inherit] [gap:inherit] outline-none"
        >
          {children}
        </AlertDialogPrimitive>
      </ModalPrimitive>
    </AlertDialogOverlay>
  )
}

function AlertDialogContent({
  className,
  size = "default",
  children,
  ...props
}: React.ComponentProps<typeof AlertDialog>) {
  return (
    <AlertDialog className={className} size={size} {...props}>
      {children}
    </AlertDialog>
  )
}

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-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]", className)}
      {...props}
    />
  )
}

function AlertDialogFooter({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-dialog-footer"
      className={cn(
        "bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
        className
      )}
      {...props}
    />
  )
}

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

function AlertDialogTitle({
  className,
  ...props
}: Omit<React.ComponentProps<typeof Heading>, "slot">) {
  return (
    <Heading
      slot="title"
      data-slot="alert-dialog-title"
      className={cn("text-base 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
}: Omit<React.ComponentProps<"div">, "slot">) {
  return (
    <div
      data-slot="alert-dialog-description"
      className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm text-balance md:text-pretty *:[a]:underline *:[a]:underline-offset-3", className)}
      {...props}
    />
  )
}

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

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

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

export default function AlertDialogDemo({
  from = "top",
}: {
  from?: "top" | "bottom" | "left" | "right"
}) {
  return (
    <AlertDialogTrigger>
      <Button variant="outline">Show Dialog</Button>
      <AlertDialog from={from}>
        <AlertDialogHeader>
          <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
          <AlertDialogDescription>
            This action cannot be undone. This will permanently delete your
            account from our servers.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Continue</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialog>
    </AlertDialogTrigger>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add alert-dialog
pnpm dlx shadcn@latest add alert-dialog
yarn dlx shadcn@latest add alert-dialog
bun x shadcn@latest add alert-dialog

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate alert-dialog
pnpm dlx @animate-ui/cli@latest animate alert-dialog
yarn dlx @animate-ui/cli@latest animate alert-dialog
bun x @animate-ui/cli@latest animate alert-dialog

Usage

React Aria builds an alert dialog from a Dialog with role="alertdialog" inside a Modal. Import from Animate UI instead of react-aria-components: it re-exports all of React Aria, with animated Modal and ModalOverlay.

import {
  Button,
  Dialog,
  DialogTrigger,
  Heading,
  Modal,
  ModalOverlay,
} from '@animate-ui/react/primitives/react-aria';

export function DeleteProject() {
  return (
    <DialogTrigger>
      <Button>Delete project</Button>
      <ModalOverlay>
        <Modal>
          <Dialog role="alertdialog">
            <Heading slot="title">Delete this project?</Heading>
            <p>This action cannot be undone.</p>
            <Button slot="close">Cancel</Button>
            <Button slot="close">Delete</Button>
          </Dialog>
        </Modal>
      </ModalOverlay>
    </DialogTrigger>
  );
}

Animation

React Aria has no Animate UI v1 counterpart: the motion is v1's Alert Dialog (Radix UI and Base UI shipped the same one), with a quicker exit.

  • ModalOverlay fades in over 400ms on a soft ease-out, along with the modal's fade, and out the same way. React Aria renders the Modal inside it, so it fades its own background color and backdrop filter (from transparent to the values its classes set) rather than its opacity, which would fade the modal with it.
  • Modal tips forward into place, around its center: it starts from perspective(500px) rotateX(-20deg) scale(0.8), transparent and blurred (4px), and comes upright and sharp on a spring (stiffness 150, damping 25), fading in ahead of it with the overlay (400ms on a soft ease-out): it's there first, then it settles. Dismissed, it goes back the way it came, at once, quicker and speeding away (0.2s, a steady push from half its average speed to one and a half), out of sight just before the overlay has cleared, then unmounts.
  • from picks the edge the modal tips from, as in v1: top by default (rotateX(-20deg)), bottom (rotateX(20deg)), left (rotateY(20deg)) or right (rotateY(-20deg)).
  • On the overlay, exitTransition defaults to transition: change transition and both directions follow, as in v1. The dialog's own exit is the quicker one above unless you set exitTransition.
  • Animate UI passes isExiting while the exit plays, so React Aria keeps the modal mounted, data-exiting stays accurate, and focus returns to the trigger.
  • Without isDismissable, an outside click doesn't close it; Escape does, unless isKeyboardDismissDisabled.
  • Reopening while it closes picks up from where it is.
  • Under reduced motion, the modal fades and un-blurs without tilting or scaling.
  • AnimateUIProvider tempo scales every duration and spring.

API Reference

Modal and ModalOverlay take exactly the props of React Aria's Modal, plus:

PropTypeDefault
from?
'top' | 'bottom' | 'left' | 'right''top'
transition?
TransitionModal: { type: 'spring', stiffness: 150, damping: 25, opacity: { duration: 0.4, ease: [0.215, 0.61, 0.355, 1] } }; ModalOverlay: { duration: 0.4, ease: [0.215, 0.61, 0.355, 1] }
exitTransition?
TransitionModal: { duration: 0.2, ease: [0.33, 0.17, 0.67, 0.5] }; ModalOverlay: transition

DialogTrigger, Dialog, Heading and the other components take React Aria props only. They are not animated.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page