Skip to content
WANCRAFT

B.158Cards & Containers

Animated Dialog

A native <dialog> modal that scales in from the button that opened it, with a blurred backdrop, alertdialog mode and nesting.

NewReactMotionTailwindintermediate

Overview

Animated Dialog is a general-purpose modal built on the native <dialog> element and showModal(), so the browser handles the top layer, the inert page behind it and focus containment. What it adds is care: the panel grows out of the exact button that opened it and shrinks back towards it on close, over a backdrop that fades and blurs in. It has header, body and footer slots, three sizes, a close button and an AnimatedDialogClose helper for footer actions. Escape and backdrop clicks can each be turned off. The alertdialog variant puts focus on the least destructive action, page scroll is locked while open (nested dialogs share one lock), focus returns to the trigger, and dialogs nest. Use it controlled with open / onOpenChange or hand it a trigger. With reduced motion it simply fades.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/animated-dialog.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/animated-dialog.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/animated-dialog.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { AnimatedDialog, AnimatedDialogClose } from "@/components/ui/animated-dialog";

export function AnimatedDialogExample() {
  return (
    <AnimatedDialog
      title="Edit profile"
      description="Changes are saved when you press Done."
      trigger={<button className="rounded-lg border px-4 py-2">Edit profile</button>}
      footer={<AnimatedDialogClose className="rounded-lg bg-zinc-900 px-4 py-2 text-white">Done</AnimatedDialogClose>}
    >
      <p>Your name and handle appear on every component you publish.</p>
    </AnimatedDialog>
  );
}

03Props

PropTypeDefaultDescription
titleReactNode—Required. Heading; labels the dialog (aria-labelledby).
descriptionReactNode—Supporting line under the title (aria-describedby).
childrenReactNode—Body content; scrolls when it is taller than the viewport.
footerReactNode—Actions row. AnimatedDialogClose buttons inside it close the dialog.
triggerReactElement—Uncontrolled API: the element that opens the dialog. It receives onClick, aria-haspopup, aria-expanded and aria-controls.
openboolean—Controlled open state (pair with onOpenChange).
defaultOpenbooleanfalseInitial state when uncontrolled.
onOpenChange(open: boolean) => void—Called when the trigger, a close action, Escape or the backdrop asks to change the state.
role"dialog" | "alertdialog""dialog"alertdialog for confirmations; focus starts on the first AnimatedDialogClose in the footer (your Cancel).
size"sm" | "md" | "lg""md"Max width 400 / 520 / 680px.
closeOnEscapebooleantrueEscape closes the dialog.
closeOnBackdropbooleantrueA click that starts and ends outside the panel closes it.
showClosebooleantrueShow the × button in the header.
blurnumber6Backdrop blur in px.
spring{ stiffness; damping; mass }{ 380, 30, 0.8 }Spring for the scale-in from the trigger.
initialFocusRefObject<HTMLElement | null>—Element to focus on open. Otherwise [data-autofocus], then a sensible default.
accentstring"#FF7448"Focus ring colour.
classNamestring—Extra classes for the panel.

04Features

  • 01Native <dialog> + showModal(): top layer, inert background, browser focus containment
  • 02Origin-aware open: the panel scales out of the trigger's position and shrinks back towards it
  • 03Backdrop that fades and blurs in; header, body and footer slots; three sizes
  • 04Alertdialog variant with focus on the least destructive action
  • 05Scroll lock with scrollbar compensation, shared by nested dialogs
  • 06Controlled (open / onOpenChange) or uncontrolled with a trigger element
motion · springmotion · tweeninput · clickinput · keyboard

05Accessibility

  • Modal <dialog> opened with showModal(): the rest of the page is inert and focus stays inside; it is labelled by its title and described by its description.
  • role="alertdialog" variant for confirmations; focus starts on the first AnimatedDialogClose in the footer (Cancel), never on the destructive action.
  • Focus moves to an explicit initialFocus or [data-autofocus] element, otherwise the first control in the body; it returns to the trigger on close.
  • Escape closes (configurable), the × button has an accessible name, and the trigger exposes aria-haspopup="dialog", aria-expanded and aria-controls.
  • With prefers-reduced-motion the panel and backdrop only fade.

06Limitations

  • Content stays mounted while the dialog is closed; render it conditionally if it is expensive.