Componentfeedback
Dialog
Accessible modal dialog with focus trap and scale animation.
Preview
Live Preview
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add dialogOr install the full npm package:
npm install bigbulluiUsage
const [open, setOpen] = useState(false);
<Button onClick={() => setOpen(true)}>Open</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogHeader>
<DialogTitle>Title</DialogTitle>
<DialogDescription>Description.</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={() => setOpen(false)}>Got it</Button>
</DialogFooter>
</Dialog>Source code
dialog.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
const FOCUSABLE = 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
export type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, children }: DialogProps) {
const contentRef = React.useRef<HTMLDivElement>(null);
const restoreRef = React.useRef<HTMLElement | null>(null);
React.useEffect(() => {
if (!open) return;
restoreRef.current = document.activeElement as HTMLElement | null;
const content = contentRef.current;
const focusables = content?.querySelectorAll<HTMLElement>(FOCUSABLE);
(focusables?.[0] ?? content)?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
onOpenChange(false);
return;
}
if (event.key !== "Tab" || !content) return;
const items = Array.from(content.querySelectorAll<HTMLElement>(FOCUSABLE));
if (items.length === 0) return;
const first = items[0]!;
const last = items[items.length - 1]!;
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
};
document.addEventListener("keydown", onKeyDown);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKeyDown);
document.body.style.overflow = "";
restoreRef.current?.focus();
};
}, [open, onOpenChange]);
if (!open) return null;
return (
<div className="fixed inset-0 z-50">
<div
aria-hidden
onClick={() => onOpenChange(false)}
className="absolute inset-0 bg-black/50 backdrop-blur-[2px] animate-[fade-in_0.2s_ease-out_both]"
/>
<div
ref={contentRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
className={cn(
"absolute left-1/2 top-1/2 w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-lg border-[1.5px] border-foreground bg-card p-6 shadow-lg outline-1 outline-dashed outline-offset-[-7px] focus:outline-none animate-[scale-in_0.2s_ease-out_both] motion-reduce:animate-none"
)}
>
{children}
<button
type="button"
aria-label="Close"
onClick={() => onOpenChange(false)}
className="absolute right-4 top-4 cursor-pointer rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
</div>
);
}
export function DialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("mb-4 flex flex-col gap-1.5 text-left", className)} {...props} />;
}
export function DialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return <h2 className={cn("text-lg font-semibold tracking-tight", className)} {...props} />;
}
export function DialogDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
}
export function DialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn("mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)} {...props} />;
}
Props
| Prop | Type | Description |
|---|---|---|
| open | boolean | Whether the dialog is open (controlled). |
| onOpenChange | (open: boolean) => void | Called when open state should change. |