Modal
Dialog overlay with configurable sizes.
Code
components/Modal.astro
---interface Props { id: string; size?: "sm" | "md" | "lg"; open?: boolean; class?: string;} const { id, size = "md", open = false, class: className = "",} = Astro.props;--- <div id={id} class={`modal-overlay fixed inset-0 z-50 bg-black-onyx/50 flex items-center justify-center p-4 ${open ? "open" : "hidden"}`} data-modal role="presentation"> <div class={`modal bg-white-ghost rounded w-full max-h-[85vh] flex flex-col ${size === "sm" ? "max-w-sm" : size === "md" ? "max-w-md" : "max-w-lg"} ${className}`} role="dialog" aria-modal="true" aria-labelledby={`${id}-title`}> <div class="modal-header flex items-center justify-between px-6 py-4 border-b border-black-onyx/10"> <div id={`${id}-title`}> <slot name="header" /> </div> <button class="modal-close text-black-onyx/40 hover:text-black-onyx transition-colors cursor-pointer" aria-label="Close"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256"> <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path> </svg> </button> </div> <div class="modal-body px-6 py-4 overflow-y-auto"> <slot /> </div> </div></div> <script> document.querySelectorAll("[data-modal]").forEach((overlay) => { const modal = overlay.querySelector(".modal"); const closeBtn = overlay.querySelector(".modal-close"); let previousFocus = null; function openModal() { overlay.classList.add("open"); overlay.classList.remove("hidden"); document.body.style.overflow = "hidden"; previousFocus = document.activeElement; modal?.focus(); } function closeModal() { overlay.classList.remove("open"); overlay.classList.add("hidden"); document.body.style.overflow = ""; if (previousFocus) { previousFocus.focus(); previousFocus = null; } } function trapFocus(e) { if (!overlay.classList.contains("open")) return; const focusable = overlay.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.key === "Tab") { if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); } } } overlay.addEventListener("keydown", (e) => { if (e.key === "Escape") closeModal(); trapFocus(e); }); closeBtn?.addEventListener("click", closeModal); overlay.addEventListener("click", (e) => { if (e.target === overlay) closeModal(); }); overlay.querySelectorAll("[data-modal-close]").forEach((el) => { el.addEventListener("click", closeModal); }); document.querySelectorAll(`[data-modal-open="${overlay.id}"]`).forEach((el) => { el.addEventListener("click", openModal); }); });</script>
Preview
Small
Medium
Large
Usage
Use data-modal-open="modal-id" on any button to open the modal. Clicking the overlay or close button dismisses it.
<Modal id="my-modal" size="md"> <h3 slot="header">Modal Title</h3> <p>Modal content goes here.</p></Modal> <button data-modal-open="my-modal"> Open Modal</button>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | — | Unique modal identifier for open/close targeting. |
| size | "sm" | "md" | "lg" | "md" | Modal width variant. |
| open | boolean | false | Initial open state. |
| class | string | — | Additional CSS classes. |