Dialog
OverlaysAn accessible modal dialog on Radix primitives — focus trap, scroll lock, escape-to-close, and animated enter/exit out of the box.
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;
export const DialogPortal = DialogPrimitive.Portal;
export function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
className,
)}
{...props}
/>
);
}
export function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
"rounded-xl border border-zinc-200 bg-white p-6 shadow-2xl outline-none dark:border-zinc-800 dark:bg-zinc-900",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
className="absolute top-4 right-4 rounded-md p-1 text-zinc-500 opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-zinc-950/60 focus-visible:outline-none dark:text-zinc-400 dark:focus-visible:ring-zinc-50/60"
aria-label="Close"
>
<X className="size-4" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
export function DialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("mb-4 flex flex-col gap-1.5", className)} {...props} />
);
}
export function DialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
export function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn(
"text-lg font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
className,
)}
{...props}
/>
);
}
export function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
);
}Overview
An accessible modal dialog composed from Radix primitives. It ships the hard parts
already wired up: focus trap, scroll lock, Escape to close, click-outside to
dismiss, and animated enter/exit.
Composition
The component is compositional rather than a single monolith — you assemble it
from named parts: Dialog, DialogTrigger, DialogContent, DialogHeader,
DialogTitle, DialogDescription, DialogFooter, and DialogClose. This keeps
markup explicit and lets you place actions wherever you need them.
Wrap your trigger and close buttons with asChild to reuse the Button component
without nesting interactive elements.
Accessibility
DialogTitle and DialogDescription are wired to aria-labelledby /
aria-describedby automatically. Always include a DialogTitle — Radix warns at
runtime if one is missing, since assistive tech relies on it.
Animation
Enter/exit transitions use tw-animate-css data-state utilities, so the dialog
fades and zooms cleanly without any JavaScript animation code.
Installation
npx shadcn@latest add https://ui.saumyarex.xyz/r/dialog.json1. Install dependencies
npm install @radix-ui/react-dialog lucide-react clsx tailwind-merge tw-animate-css2. Copy the source into your project
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
export const Dialog = DialogPrimitive.Root;
export const DialogTrigger = DialogPrimitive.Trigger;
export const DialogClose = DialogPrimitive.Close;
export const DialogPortal = DialogPrimitive.Portal;
export function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
className,
)}
{...props}
/>
);
}
export function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"fixed top-1/2 left-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
"rounded-xl border border-zinc-200 bg-white p-6 shadow-2xl outline-none dark:border-zinc-800 dark:bg-zinc-900",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0",
"data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close
className="absolute top-4 right-4 rounded-md p-1 text-zinc-500 opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-zinc-950/60 focus-visible:outline-none dark:text-zinc-400 dark:focus-visible:ring-zinc-50/60"
aria-label="Close"
>
<X className="size-4" />
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
);
}
export function DialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div className={cn("mb-4 flex flex-col gap-1.5", className)} {...props} />
);
}
export function DialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
className={cn(
"mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className,
)}
{...props}
/>
);
}
export function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
className={cn(
"text-lg font-semibold tracking-tight text-zinc-950 dark:text-zinc-50",
className,
)}
{...props}
/>
);
}
export function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn("text-sm text-zinc-500 dark:text-zinc-400", className)}
{...props}
/>
);
}import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/** Merge conditional class names and resolve Tailwind conflicts. */
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/** Shared view-transition name so a card preview morphs into the detail
* page's preview. Must match on both ends; unique per registry entry. */
export function previewTransitionName(name: string) {
return `preview-${name}`;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
Dialog / DialogTrigger / DialogClose | Radix primitives | — | Root, trigger, and close. Re-exported from @radix-ui/react-dialog. |
DialogContent | React.ComponentProps<typeof Dialog.Content> | — | The animated panel — includes overlay, close button, focus trap, scroll lock. |
DialogHeader / Footer / Title / Description | layout + a11y slots | — | Composable building blocks; Title/Description wire up aria attributes. |