Modal
Content overlay without leaving the page
Preview
The real canon modal: backdrop, focus trap, close via Escape / backdrop click / "Close" button, locked scroll and Lenis stop/start. The trigger is a <button> with data-modal pointing to the id.
Props
| Prop | Type | Note |
|---|---|---|
id | string | Required. Triggers use data-modal="id". |
title | string | Required. Rendered as <p>, not a heading. |
image, imageAlt | string | Optional. One image at most, on top, clipped to the panel corners. |
metadata | string | Optional. Inline with "·". |
closeLabel | string | Default "Cerrar" (this page passes "Close"). |
slot body | n/a | Body paragraphs (3 at most). |
Anatomy
| Element | Family | Desktop | Mobile |
|---|---|---|---|
| Backdrop | n/a | #000 at 60% | Same |
| Panel | n/a | max-width 720px, radius 16px | Same |
| Title | Aleo | 28px / 400 | 22px |
| Body | Georama | 16px / 400 | 16px |
| Metadata | Georama | 12px / 400, secondary (--text-caption) | 12px |
| Close button | Aleo | Text label, 18px, bottom-right | Full width, centered |
Behavior
| Action | Animation |
|---|---|
| Open | Backdrop + panel fade in, panel scale 0.98→1, 0.3s ease-out |
| Close | Fade out 0.2s ease-in |
| Ways to close | Backdrop click, "Close" button, Escape key |
Body scroll is locked. Focus is trapped inside the modal. Lenis stop/start.
Constraints
- Three body paragraphs at most.
- One image at most. No gallery.
- No forms, no video, no nested modals.
- No navigation between modals.
- No URL change. Modals are ephemeral.
display: nonewhen closed, notopacity: 0(avoids preloading images).
Decisions
| Decision | Why |
|---|---|
| A "Close" text label, not a × icon | Typographic approach. More explicit. |
| Close at the bottom, not top-right | The reader finishes the content and finds the way out |
| Max 3 paragraphs | Forces curation. Long content deserves its own page |
| display:none, not opacity:0 | Stops the browser from preloading images in hidden modals |
| <button> as trigger, not <a> | Modals don't navigate. An <a> with an empty href fails accessibility |