Dialog
A modal dialog with header, title, description, and footer sections.
Demo
Dependencies
lucide-reactclsxtailwind-merge
Props
This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().
Source
"use client";
import { useCallback, useEffect, useId, useRef } from "react";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";
export interface DialogProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children?: React.ReactNode;
}
export function Dialog({ open, onOpenChange, children }: DialogProps) {
return (
<DialogContext.Provider value={{ open, onOpenChange }}>
{children}
</DialogContext.Provider>
);
}
import { createContext, useContext } from "react";
interface DialogContextValue {
open?: boolean;
onOpenChange?: (open: boolean) => void;
}
const DialogContext = createContext<DialogContextValue>({});
export function DialogTrigger({
children,
asChild,
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
asChild?: boolean;
}) {
const { onOpenChange } = useContext(DialogContext);
if (asChild) {
return (
<span onClick={() => onOpenChange?.(true)} className={className}>
{children}
</span>
);
}
return (
<button
onClick={() => onOpenChange?.(true)}
className={className}
{...props}
>
{children}
</button>
);
}
export function DialogContent({
children,
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
const { open, onOpenChange } = useContext(DialogContext);
const id = useId();
const overlayRef = useRef<HTMLDivElement>(null);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape") onOpenChange?.(false);
},
[onOpenChange],
);
useEffect(() => {
if (open) {
document.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "";
};
}, [open, handleKeyDown]);
if (!open) return null;
return (
<div
ref={overlayRef}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={(e) => {
if (e.target === overlayRef.current) onOpenChange?.(false);
}}
>
<div
role="dialog"
aria-modal="true"
aria-labelledby={`${id}-title`}
className={cn(
"relative w-full max-w-lg rounded-lg border border-gray-200 bg-white p-6 shadow-sm",
className,
)}
{...props}
>
<button
onClick={() => onOpenChange?.(false)}
className="absolute right-4 top-4 rounded-sm text-gray-500 hover:text-gray-900"
aria-label="Close"
>
<X className="h-4 w-4" />
</button>
{children}
</div>
</div>
);
}
export function DialogHeader({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("mb-4 space-y-1.5", className)} {...props} />;
}
export function DialogTitle({
className,
...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
const id = useId();
return (
<h2
id={id}
className={cn(
"text-lg font-semibold leading-none tracking-tight text-gray-900",
className,
)}
{...props}
/>
);
}
export function DialogDescription({
className,
...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
return <p className={cn("text-sm text-gray-500", className)} {...props} />;
}
export function DialogFooter({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"mt-6 flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-2",
className,
)}
{...props}
/>
);
}
CLI
npx genui add dialog