Modal & Drawer System Generator
Create accessible, polished modal dialogs and drawer components.
Core Workflow Choose type: Modal (center), Drawer (side), Bottom Sheet Setup portal: Render outside DOM hierarchy Focus management: Focus trap and restoration Accessibility: ARIA attributes, keyboard shortcuts Animations: Smooth enter/exit transitions Scroll lock: Prevent body scroll when open Backdrop: Click outside to close Base Modal Component "use client";
import { useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { X } from "lucide-react";
interface ModalProps { isOpen: boolean; onClose: () => void; title?: string; description?: string; children: React.ReactNode; size?: "sm" | "md" | "lg" | "xl" | "full"; closeOnEscape?: boolean; closeOnBackdrop?: boolean; }
export function Modal({
isOpen,
onClose,
title,
description,
children,
size = "md",
closeOnEscape = true,
closeOnBackdrop = true,
}: ModalProps) {
const modalRef = useRef
// ESC key handler useEffect(() => { if (!isOpen || !closeOnEscape) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen, closeOnEscape, onClose]);
// Focus trap useEffect(() => { if (!isOpen) return;
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements?.[0] as HTMLElement;
const lastElement = focusableElements?.[
focusableElements.length - 1
] as HTMLElement;
const handleTab = (e: KeyboardEvent) => {
if (e.key !== "Tab") return;
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement?.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement?.focus();
}
};
firstElement?.focus();
document.addEventListener("keydown", handleTab);
return () => document.removeEventListener("keydown", handleTab);
}, [isOpen]);
// Body scroll lock useEffect(() => { if (isOpen) { document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [isOpen]);
if (!isOpen) return null;
const sizeClasses = { sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", full: "max-w-full mx-4", };
return createPortal(
{/* Modal */}
<div
ref={modalRef}
className={`relative z-10 w-full ${sizeClasses[size]} animate-in zoom-in-95 slide-in-from-bottom-4 duration-200`}
>
<div className="rounded-lg bg-white shadow-xl">
{/* Header */}
{(title || description) && (
<div className="border-b px-6 py-4">
{title && (
<h2 id="modal-title" className="text-xl font-semibold">
{title}
</h2>
)}
{description && (
<p
id="modal-description"
className="mt-1 text-sm text-gray-600"
>
{description}
</p>
)}
</div>
)}
{/* Close button */}
<button
onClick={onClose}
className="absolute right-4 top-4 rounded-lg p-1 hover:bg-gray-100"
aria-label="Close modal"
>
<X className="h-5 w-5" />
</button>
{/* Content */}
<div className="p-6">{children}</div>
</div>
</div>
</div>,
document.body
); }
Drawer Component interface DrawerProps { isOpen: boolean; onClose: () => void; position?: "left" | "right" | "bottom"; title?: string; children: React.ReactNode; }
export function Drawer({ isOpen, onClose, position = "right", title, children, }: DrawerProps) { // Similar hooks as Modal (ESC, focus trap, scroll lock)
const positionClasses = { left: "left-0 top-0 h-full w-80 animate-in slide-in-from-left", right: "right-0 top-0 h-full w-80 animate-in slide-in-from-right", bottom: "bottom-0 left-0 right-0 h-96 animate-in slide-in-from-bottom", };
if (!isOpen) return null;
return createPortal(
{title}
Common Use Cases Confirmation Dialog interface ConfirmDialogProps { isOpen: boolean; onClose: () => void; onConfirm: () => void; title: string; description: string; confirmText?: string; cancelText?: string; variant?: "danger" | "default"; }
export function ConfirmDialog({
isOpen,
onClose,
onConfirm,
title,
description,
confirmText = "Confirm",
cancelText = "Cancel",
variant = "default",
}: ConfirmDialogProps) {
return (
{description}{title}
Edit Form Modal export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) { const [isSaving, setIsSaving] = useState(false);
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSaving(true); // Save logic onClose(); };
return (
Detail View Drawer
export function UserDetailDrawer({
user,
isOpen,
onClose,
}: UserDetailDrawerProps) {
return (
{user.email} {user.role} {user.department} {formatDate(user.joinedAt)}{user.name}
Role
Department
Joined
Best Practices Portal rendering: Render outside parent DOM Focus management: Trap focus, restore on close Keyboard support: ESC to close, Tab navigation ARIA attributes: role, aria-modal, aria-labelledby Scroll lock: Prevent body scroll when open Backdrop: Click outside to close (optional) Animations: Smooth enter/exit transitions Mobile responsive: Full screen on small devices Output Checklist Modal component with portal Drawer component (left/right/bottom) Focus trap implementation ESC key handler Scroll lock on body Backdrop with click-to-close ARIA attributes Smooth animations Close button Sample use cases (confirm, edit, detail)
← 返回排行榜