Modal
ComponentsReactTailwindDialog overlay with backdrop blur, Esc/click-outside dismiss, body scroll lock, composable header/title/description/footer. Four variants: info, confirm, form, destructive.
Modal Variants
Backdrop blur, Esc to close, click outside to dismiss, body scroll locked while open.
1 "use client"; 2 3 import { useEffect, useState } from "react"; 4 import { 5 Modal, 6 ModalHeader, 7 ModalTitle, 8 ModalDescription, 9 ModalFooter, 10 } from "@/shared/ui/modal"; 11 import { Button } from "@/shared/ui/button"; 12 import { Input } from "@/shared/ui/input"; 13 import { Label } from "@/shared/ui/label"; 14 15 // Modal primitive (shared/ui/modal.tsx) 16 type ModalProps = { 17 open: boolean; 18 onClose: () => void; 19 children: React.ReactNode; 20 className?: string; 21 }; 22 23 export function Modal({ open, onClose, children, className }: ModalProps) { 24 useEffect(() => { 25 if (!open) return; 26 const handleEsc = (e: KeyboardEvent) => e.key === "Escape" && onClose(); 27 document.addEventListener("keydown", handleEsc); 28 document.body.style.overflow = "hidden"; 29 return () => { 30 document.removeEventListener("keydown", handleEsc); 31 document.body.style.overflow = ""; 32 }; 33 }, [open, onClose]); 34 35 if (!open) return null; 36 37 return ( 38 <div className="fixed inset-0 z-50 flex items-center justify-center"> 39 <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} /> 40 <div className="bg-card relative z-10 mx-4 w-full max-w-lg rounded-lg border p-6 shadow-lg"> 41 {children} 42 </div> 43 </div> 44 ); 45 } 46 47 // Info modal usage (icon badge + bullet list) 48 <Modal open={infoOpen} onClose={() => setInfoOpen(false)}> 49 <ModalHeader> 50 <div className="bg-primary/10 mb-3 inline-flex h-9 w-9 items-center justify-center rounded-full"> 51 <FontAwesomeIcon icon={faCircleInfo} className="text-primary h-4 w-4" /> 52 </div> 53 <ModalTitle>What's new in v2.0</ModalTitle> 54 <ModalDescription> 55 Major improvements to performance, accessibility, and theming. 56 </ModalDescription> 57 </ModalHeader> 58 <ul className="text-foreground space-y-2 text-sm"> 59 <li className="flex gap-2"> 60 <span className="text-primary">•</span> 61 React 19 + React Compiler support 62 </li> 63 <li className="flex gap-2"> 64 <span className="text-primary">•</span> 65 Tailwind v4 oklch theme tokens 66 </li> 67 </ul> 68 <ModalFooter> 69 <Button onClick={() => setInfoOpen(false)}>Got it</Button> 70 </ModalFooter> 71 </Modal> 72 73 // Confirm modal usage 74 export function ConfirmExample() { 75 const [open, setOpen] = useState(false); 76 77 return ( 78 <> 79 <Button onClick={() => setOpen(true)}>Open confirm</Button> 80 <Modal open={open} onClose={() => setOpen(false)}> 81 <ModalHeader> 82 <ModalTitle>Save changes?</ModalTitle> 83 <ModalDescription> 84 You have unsaved edits. Save them before leaving? 85 </ModalDescription> 86 </ModalHeader> 87 <ModalFooter> 88 <Button variant="ghost" onClick={() => setOpen(false)}>Discard</Button> 89 <Button onClick={() => setOpen(false)}>Save changes</Button> 90 </ModalFooter> 91 </Modal> 92 </> 93 ); 94 } 95 96 // Form modal usage (Label + Input, create disabled until filled) 97 <Modal open={formOpen} onClose={() => setFormOpen(false)}> 98 <ModalHeader> 99 <ModalTitle>Create new project</ModalTitle> 100 <ModalDescription> 101 Give your project a unique name. You can rename it later. 102 </ModalDescription> 103 </ModalHeader> 104 <div className="space-y-1.5"> 105 <Label htmlFor="project-name">Project name</Label> 106 <Input 107 id="project-name" 108 value={projectName} 109 onChange={(e) => setProjectName(e.target.value)} 110 placeholder="my-awesome-app" 111 autoFocus 112 /> 113 </div> 114 <ModalFooter> 115 <Button variant="ghost" onClick={() => setFormOpen(false)}>Cancel</Button> 116 <Button disabled={!projectName.trim()} onClick={handleCreate}> 117 Create project 118 </Button> 119 </ModalFooter> 120 </Modal> 121 122 // Destructive modal usage 123 <Modal open={deleteOpen} onClose={() => setDeleteOpen(false)}> 124 <ModalHeader> 125 <ModalTitle>Delete project?</ModalTitle> 126 <ModalDescription> 127 This permanently deletes the project. Cannot be undone. 128 </ModalDescription> 129 </ModalHeader> 130 <ModalFooter> 131 <Button variant="ghost" onClick={() => setDeleteOpen(false)}>Cancel</Button> 132 <Button variant="destructive" onClick={handleDelete}>Yes, delete</Button> 133 </ModalFooter> 134 </Modal>