Dialog

An overlay shown above other content in an application. Also the primitive under shadcn/ui's Sheet.

"use client"

import * as React from "react"
import { cn } from "cn"
import {
  Dialog as DialogPrimitive,
  DialogTrigger as DialogTriggerPrimitive,
  Heading,
  ModalOverlay as ModalOverlayPrimitive,
  Modal as ModalPrimitive,
  type DialogProps as DialogPrimitiveProps,
  type DialogTriggerProps as DialogTriggerPrimitiveProps,
  type ModalOverlayProps as ModalOverlayPrimitiveProps,
} from "@animate-ui/react/primitives/react-aria"

import { Button } from "@/components/ui/button"
import { Field, FieldGroup } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Cancel01Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"

function DialogTrigger({ ...props }: DialogTriggerPrimitiveProps) {
  return <DialogTriggerPrimitive data-slot="dialog-trigger" {...props} />
}

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

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

function Dialog({
  className,
  children,
  showCloseButton = true,
  isDismissable = true,
  from,
  ...props
}: Omit<ModalOverlayPrimitiveProps, "className" | "children"> &
  Pick<React.ComponentProps<typeof ModalPrimitive>, "isDismissable" | "from"> & {
    className?: string
    children: React.ReactNode
    showCloseButton?: boolean
  }) {
  return (
    <DialogOverlay isDismissable={isDismissable} {...props}>
      <ModalPrimitive
        data-slot="dialog-content"
        from={from}
        className={cn(
          "bg-popover text-popover-foreground ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
          className
        )}
      >
        <DialogPrimitive
          data-slot="dialog"
          className="[display:inherit] [gap:inherit] outline-none"
        >
          {children}
          {showCloseButton && (
            <DialogClose
              variant="ghost"
              className="absolute top-2 right-2"
              size="icon-sm"
            >
              <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2}
              />
              <span className="sr-only">Close</span>
            </DialogClose>
          )}
        </DialogPrimitive>
      </ModalPrimitive>
    </DialogOverlay>
  )
}

function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="dialog-header"
      className={cn("gap-2 flex flex-col", className)}
      {...props}
    />
  )
}

function DialogFooter({
  className,
  showCloseButton = false,
  children,
  ...props
}: React.ComponentProps<"div"> & {
  showCloseButton?: boolean
}) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn(
        "bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
        className
      )}
      {...props}
    >
      {children}
      {showCloseButton && <DialogClose variant="outline">Close</DialogClose>}
    </div>
  )
}

function DialogTitle({
  className,
  ...props
}: Omit<React.ComponentProps<typeof Heading>, "slot">) {
  return (
    <Heading
      slot="title"
      data-slot="dialog-title"
      className={cn("text-base leading-none font-medium", className)}
      {...props}
    />
  )
}

function DialogDescription({
  className,
  ...props
}: Omit<React.ComponentProps<"div">, "slot">) {
  return (
    <div
      data-slot="dialog-description"
      className={cn("text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3", className)}
      {...props}
    />
  )
}

export {
  type DialogPrimitiveProps,
  type DialogTriggerPrimitiveProps,
  Dialog,
  DialogClose,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogOverlay,
  DialogTitle,
  DialogTrigger,
}

export default function DialogDemo({
  from = "top",
  showCloseButton = true,
}: {
  from?: "top" | "bottom" | "left" | "right"
  showCloseButton?: boolean
}) {
  return (
    <DialogTrigger>
      <form>
        <Button variant="outline">Open Dialog</Button>
        <Dialog
          className="sm:max-w-sm"
          from={from}
          showCloseButton={showCloseButton}
        >
          <DialogHeader>
            <DialogTitle>Edit profile</DialogTitle>
            <DialogDescription>
              Make changes to your profile here. Click save when you&apos;re
              done.
            </DialogDescription>
          </DialogHeader>
          <FieldGroup>
            <Field>
              <Label htmlFor="name-1">Name</Label>
              <Input id="name-1" name="name" defaultValue="Pedro Duarte" />
            </Field>
            <Field>
              <Label htmlFor="username-1">Username</Label>
              <Input id="username-1" name="username" defaultValue="@peduarte" />
            </Field>
          </FieldGroup>
          <DialogFooter>
            <DialogClose variant="outline">Cancel</DialogClose>
            <Button type="submit">Save changes</Button>
          </DialogFooter>
        </Dialog>
      </form>
    </DialogTrigger>
  )
}

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 dialog
pnpm dlx shadcn@latest add dialog
yarn dlx shadcn@latest add dialog
bun x shadcn@latest add 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 dialog
pnpm dlx @animate-ui/cli@latest animate dialog
yarn dlx @animate-ui/cli@latest animate dialog
bun x @animate-ui/cli@latest animate dialog

Usage

Import from Animate UI instead of react-aria-components: it re-exports all of React Aria, with animated Modal and ModalOverlay. Everything else stays the same.

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

export function EditProfile() {
  return (
    <DialogTrigger>
      <Button>Edit profile</Button>
      <ModalOverlay isDismissable>
        <Modal>
          <Dialog>
            <Heading slot="title">Edit profile</Heading>
            <Button slot="close">Save</Button>
          </Dialog>
        </Modal>
      </ModalOverlay>
    </DialogTrigger>
  );
}

A Modal on its own (without a ModalOverlay) animates too. DialogTrigger, Dialog and Heading are not animated.

Sheet

shadcn/ui's Sheet is built on this primitive: with data-side on its Modal, it slides in from that edge instead of tipping in. See Sheet.

Animation

React Aria has no Animate UI v1 counterpart: the motion is v1's Dialog (Radix UI, Base UI and Headless 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, where v1's overlay and content were siblings: fading its opacity (or filter) would fade and blur the modal too and cut its exit short. So it fades its own background color and backdrop filter, from transparent to the values its classes set, and the modal moves exactly like v1's content.
  • 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)) (a sheet, with data-side, slides from its side instead).
  • On the overlay, exitTransition defaults to transition: change transition and both directions follow, as in v1. A dialog's or a sheet's own exit is the quicker one above unless you set exitTransition.
  • React Aria only keeps a closing modal mounted if animations are running when it closes, before Motion starts any. Animate UI tracks the same open state React Aria reads (the DialogTrigger's, or isOpen/defaultOpen) and passes isExiting while the exit plays, so data-exiting and the isExiting render prop stay accurate, focus stops being trapped and returns to the trigger.
  • Reopening while it closes picks up from where it is.
  • Opening from the keyboard animates the same way; focus moves into the dialog at once.
  • className and style functions keep working, and your own render function is made animatable rather than replaced.
  • Under reduced motion, the modal fades and un-blurs without tilting, scaling or sliding.
  • AnimateUIProvider tempo scales every duration and spring.

API Reference

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

ModalOverlay

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

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