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 left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
"rounded-base border border-border bg-surface p-6 shadow-2xl outline-none",
"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 right-4 top-4 rounded-md p-1 text-muted opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:outline-none"
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-foreground", className)}
{...props}
/>
);
}
export function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn("text-sm text-muted", 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 left-1/2 top-1/2 z-50 w-full max-w-md -translate-x-1/2 -translate-y-1/2",
"rounded-base border border-border bg-surface p-6 shadow-2xl outline-none",
"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 right-4 top-4 rounded-md p-1 text-muted opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-2 focus-visible:ring-accent/60 focus-visible:outline-none"
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-foreground", className)}
{...props}
/>
);
}
export function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
className={cn("text-sm text-muted", 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));
}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. |