--- name: modal-drawer-system description: Implements accessible modals and drawers with focus trap, ESC to close, scroll lock, portal rendering, and ARIA attributes. Includes sample implementations for common use cases like edit forms, confirmations, and detail views. Use when building "modals", "dialogs", "drawers", "sidebars", or "overlays". --- # Modal & Drawer System Generator Create accessible, polished modal dialogs and drawer components. ## Core Workflow 1. **Choose type**: Modal (center), Drawer (side), Bottom Sheet 2. **Setup portal**: Render outside DOM hierarchy 3. **Focus management**: Focus trap and restoration 4. **Accessibility**: ARIA attributes, keyboard shortcuts 5. **Animations**: Smooth enter/exit transitions 6. **Scroll lock**: Prevent body scroll when open 7. **Backdrop**: Click outside to close ## Base Modal Component ```typescript "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(null); // 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(
{/* Backdrop */}
{/* Modal */}
{/* Header */} {(title || description) && (
{title && ( )} {description && ( )}
)} {/* Close button */} {/* Content */}
{children}
, document.body ); } ``` ## Drawer Component ```typescript 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}

{children}
, document.body ); } ``` ## Common Use Cases ### Confirmation Dialog ```typescript 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 (

{title}

{description}

); } ``` ### Edit Form Modal ```typescript 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 ```typescript export function UserDetailDrawer({ user, isOpen, onClose, }: UserDetailDrawerProps) { return (

{user.name}

{user.email}

Role

{user.role}

Department

{user.department}

Joined

{formatDate(user.joinedAt)}

); } ``` ## Best Practices 1. **Portal rendering**: Render outside parent DOM 2. **Focus management**: Trap focus, restore on close 3. **Keyboard support**: ESC to close, Tab navigation 4. **ARIA attributes**: role, aria-modal, aria-labelledby 5. **Scroll lock**: Prevent body scroll when open 6. **Backdrop**: Click outside to close (optional) 7. **Animations**: Smooth enter/exit transitions 8. **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)