Click-to-enlarge lightbox for article figures

Clicking a figure image opens a full-viewport overlay: the image as
large as fits (100vw, flex-shrunk to leave exactly the caption's
height) with the caption below. Click or any key closes it; wheel and
touch scrolling stop at the overlay (overscroll-behavior: contain).

Styling lives in the base theme: blurred dark backdrop (theme-tunable
via --lightbox-bg / --lightbox-text), soft shadow, reduced-motion-aware
open animation.
This commit is contained in:
2026-08-30 02:08:06 +00:00
parent 8fc5dd7b9b
commit 7e8e0a2ea7
3 changed files with 117 additions and 1 deletions
+74
View File
@@ -1346,6 +1346,80 @@ figure:has(.margin) {
margin: 0.3rem 1em 1rem 0;
}
/* Click-to-enlarge (pagerite.js): article figure images open in a
full-viewport lightbox — the image as large as fits with the caption
below; click or Esc closes. The backdrop keeps a hint of the page
behind it (blur + dark tint) rather than going solid black; themes
retune it via --lightbox-bg / --lightbox-text. */
article figure img {
cursor: zoom-in;
}
#lightbox {
--lightbox-bg: rgb(0 0 0 / 0.68);
--lightbox-text: #e8e8e8;
position: fixed;
inset: 0;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--lightbox-bg);
backdrop-filter: blur(1rem) saturate(0.85);
/* Wheel/touch scrolling stops at the overlay instead of chaining to
the page behind it. overscroll-behavior needs a scroll container,
hence overflow: auto — the content is clamped to fit, so it never
actually scrolls. */
overflow: auto;
overscroll-behavior: contain;
cursor: zoom-out;
}
#lightbox img {
max-width: 100vw;
/* Flex-shrink does the vertical fit: the image yields exactly the
space the caption needs, no fixed reservation. */
max-height: 100%;
flex: 0 1 auto;
min-height: 0;
object-fit: contain;
border-radius: 3px;
box-shadow: 0 1rem 4rem rgb(0 0 0 / 0.55);
}
#lightbox .caption {
max-width: 65ch;
padding: 0.8rem 1rem;
color: var(--lightbox-text);
font-size: 0.95rem;
text-align: center;
opacity: 0.85;
}
@media (prefers-reduced-motion: no-preference) {
#lightbox {
animation: lightbox-in 0.18s ease-out;
}
#lightbox img {
animation: lightbox-img 0.22s ease-out;
}
@keyframes lightbox-in {
from {
opacity: 0;
}
}
@keyframes lightbox-img {
from {
opacity: 0;
transform: scale(0.96);
}
}
}
/* Wide single-column pages: margin boxes lean into the vacant left
gutter instead (below 104rem the gutter cannot hold the box, and while
editing the docked panel reshapes the gutters — in both they stay
+42
View File
@@ -268,6 +268,41 @@ import "overlayscrollbars/overlayscrollbars.css";
}
}
// --- Figure lightbox (click to enlarge) --------------------------------
// Clicking an article figure's image opens it in a full-viewport box:
// the image as large as fits with its caption below; click or Esc
// closes. The zoomed <img> reuses the source URL — /_f/ is immutable
// and content-negotiated, so the large view comes from the browser
// cache at no extra cost.
let lightbox = null;
function closeLightbox() {
lightbox?.remove();
lightbox = null;
}
function openLightbox(figure) {
const img = figure.querySelector("img");
if (!img) return;
closeLightbox();
lightbox = document.createElement("div");
lightbox.id = "lightbox";
const big = document.createElement("img");
big.src = img.currentSrc || img.src;
big.alt = img.alt;
lightbox.append(big);
const cap = figure.querySelector("figcaption");
if (cap) {
const c = document.createElement("div");
c.className = "caption";
c.textContent = cap.textContent;
lightbox.append(c);
}
lightbox.addEventListener("click", closeLightbox);
document.body.append(lightbox);
}
// Any key dismisses the lightbox — Esc included, and the rest would
// only scroll the page behind it anyway.
addEventListener("keydown", () => closeLightbox());
// Tuck the article edit pen at the end of the first h1 (which may come
// from the markdown itself). Re-runs when the editor replaces the
// previewed article, since that wipes elements inside it.
@@ -822,6 +857,13 @@ import "overlayscrollbars/overlayscrollbars.css";
.catch((e) => console.error("editor load failed:", e));
return;
}
// Article figure images enlarge into the lightbox.
const fig = ev.target.closest("#main article figure");
if (fig && ev.target.closest("img")) {
ev.preventDefault();
openLightbox(fig);
return;
}
const a = ev.target.closest("a[href]");
if (!a || a.target || a.hasAttribute("download")) return;
const url = new URL(a.href, location.href);