Plate № 23 · UX · media / lightbox
A pattern from the gf.cx specimen book
A closer look, one click away
"can we convert this to a pattern-modal, and store it on assets.gf, and talk about it on patterns.gf?"
The problem
A page grows a thumbnail — a locator map, a facility photo, a hand-drawn diagram — and the reader wants it bigger. So you build a modal: an overlay, a close button, backdrop-and-Escape handling, focus return, body-scroll lock, a caption. It works. Then the next page needs one, and the one after that, and each gets its own slightly-different copy: this one traps focus, that one forgets to restore scroll, a third uses a z-index that a tooltip pokes through. The behaviour is identical every time; only the bugs differ.
The same drift the provenance footer and back-to-top icon kill, one layer up: a UI affordance that should be one shared thing, hand-copied into many. The fix is the same — lift it to a single CDN-hosted primitive, mark the thumbnail with a data attribute, and let the shelf own the overlay.
The element, in-page
The live specimen below runs in a sandboxed iframe (the pattern-preview primitive). Click either thumbnail to open the enlargement; close with the ×, a click on the backdrop, or Escape. Tab to a thumbnail and press Enter — it opens too. One overlay serves every thumbnail on the page.
How you use it
Link the shared css + js once, then mark any thumbnail data-modal. No overlay markup, no per-page script:
<link rel="stylesheet" href="https://assets.gf.cx/modal/modal.css">
<script src="https://assets.gf.cx/modal/modal.js" defer></script>
<figure class="modal-thumb" data-modal
data-modal-src="…/map.png"
data-modal-caption="Locator — Josefstrasse 225. © OpenStreetMap">
<img src="…/map.png" alt="Locator map">
<figcaption>Josefstrasse 225</figcaption>
</figure>
What gets enlarged, first match wins: data-modal-src (an image URL) → data-modal-node (a CSS selector to clone — e.g. an inline <svg> shown small) → otherwise the trigger's own first <svg> or <img>. Caption falls back to the trigger's <figcaption>.
Series & galleries
Give related triggers the same data-modal-group="name" and the overlay gains ‹ › arrows, ArrowLeft/ArrowRight rotation (wraps around), and an n / total position counter — clicking the enlarged image also advances. Arrows and counter appear only when the group has two or more members.
For an existing thumbnail grid there is a faster path: mark the container data-modal-gallery="name" and every descendant image link inside it is promoted into that group automatically — no per-thumbnail markup. This is the one-attribute upgrade that turns a plain grid of <a href="…jpg"><img></a> links into a navigable lightbox:
<div class="thumb-grid" data-modal-gallery="submission-packet">
<a href="…/label.jpg"><img src="…/label.jpg" alt="Priority Mail label"></a>
<a href="…/mv-11.jpg"><img src="…/mv-11.jpg" alt="MV-11 completed"></a>
<a href="…/fees.pdf">Fee schedule (PDF)</a> <!-- left alone: opens normally -->
</div>
Each image link (.jpg/.jpeg/.png/.gif/.webp/.avif/.bmp/.svg) becomes a grouped trigger; its caption defaults to the thumbnail's alt, then the link's title. Non-image links (a PDF, a page) are skipped and open as usual. The <a href> is kept intact, so without JS the click still opens the raw image — the script only intercepts it. That progressive-enhancement floor is why the affordance is safe to bolt onto any legacy grid.
Where it applies
- Any thumbnail that rewards a closer look. Locator maps, facility photos, charts, hand-drawn diagrams — a
<figure>the reader might want full-size. Origin: srvc.gf.cx/transit, where facility Visuals disclosures pair an OpenStreetMap locator with a building photo, and the leaf-spine fabric primer enlarges a hand-authored inline<svg>. - Inline SVG, not just raster. The
data-modal-nodeclone path enlarges vector diagrams crisply at any size — the small thumbnail and the enlargement are the same source node, so there is nothing to keep in sync. - Article and teardown surfaces. Any page built from the shared shelf that embeds figures — video teardowns, notes, dossiers — gets enlargement by adding one attribute.
- Thumbnail grids & document galleries. A grid of image links — a submission packet, a receipts strip, a photo contact sheet — becomes a navigable lightbox by adding
data-modal-galleryto the wrapper alone. In use on home.gf.cx/vehicles, where the F250 submission packet and each vehicle's receipts strip flip through with arrows, keyboard, and a counter. - Skip where a thumbnail is already a link to a fuller page you want the reader to visit (the click belongs to navigation), or where the image is decorative and there is nothing to inspect.
Reusable elements
The named, copyable pieces — lift any one without the others:
- Shared asset
assets.gf.cx/modal/modal.css+modal.js— the complete behaviour: on-demand overlay injection, event-delegated open, image + inline-SVG enlargement, caption, ×/backdrop/Escape close, focus return, body-scroll lock, singleton guard, print hiding. Two lines in the head/foot. - The
data-modalattribute API —data-modal(marker) + optionaldata-modal-src/data-modal-node/data-modal-caption/data-modal-alt. House convention, same shape asdata-tip/data-site-nav. - Node-clone enlargement —
data-modal-node="#sel"deep-clones an existing DOM node (stripsid/width/height, scales tomin(86vw, 900px)) so an inline<svg>shown small enlarges from a single source. - Progressive-enhancement a11y — triggers lacking them are auto-given
role="button"+tabindex="0"; aMutationObserverwires thumbnails added after load. A bare<figure data-modal>is keyboard-operable with no extra markup. - Series rotation + gallery auto-wire —
data-modal-group="name"gives a set ‹ › arrows, keyboard rotation, and an n / total counter;data-modal-gallery="name"on a container promotes its image links into that group with no per-thumbnail markup, skipping non-image links and preserving the<a href>as the no-JS fallback. - The
.modal-thumbaffordance — the framed,cursor: zoom-in, focus-ringed thumbnail look, themed off local CSS vars; width/float stay page-specific.
Reference
- Source
- Dan, 2026-07-09 — "convert this to a pattern-modal, store it on assets.gf, talk about it on patterns.gf". Distilled from the bespoke modal built for
srvc.gf.cx/transitthe same day, then lifted to the shelf. - In use / example
- srvc.gf.cx/transit — facility Visuals disclosures (map + building photo) and the leaf-spine fabric diagram all open through the shared modal. home.gf.cx/vehicles — submission-packet and receipts-strip galleries (100+ thumbnails) navigate via
data-modal-gallery. Shelf source:assets.gf.cx/modal/. - Reusable elements
- Shared CDN css+js ·
data-modalattribute API · node-clone enlargement · series rotation + gallery auto-wire (data-modal-group/data-modal-gallery) · progressive-enhancement a11y ·.modal-thumbaffordance (listed above). - Origin
- An event-delegated modal hand-wired into
srvc.gf.cx/transit(2026-07-09) for locator maps + an inline-SVG diagram. Generalised the same day to a shelf primitive. Sibling of the back-to-top jump icon and built-with provenance footer — the same single-source, control-plane discipline applied to a different page layer. - Build cost
- ~30 min to lift the bespoke modal to the shared css+js and migrate the origin page; ~1 line + a
data-modalattribute per new thumbnail thereafter.