Photography — Gallery Lightbox
A gallery-dark, image-first lightbox component with a responsive thumbnail grid that opens into a full-bleed overlay. Click any frame to launch a backdrop-blurred viewer with prev/next controls, keyboard arrows and Esc, a live counter like 3/12, and a typographic caption strip. Built with vanilla JS, focus trapping, and buttery micro-interactions — drop it into any photographer portfolio or editorial spread.
MCP
Code
:root {
--ink: #0b0b0c;
--bg: #101012;
--surface: #17171b;
--surface-2: #1e1e23;
--paper: #f6f4ef;
--sand: #c9b79c;
--sand-d: #a8916f;
--muted: #9a9aa2;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--paper);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.grain {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
opacity: 0.5;
background:
radial-gradient(120% 90% at 50% -10%, rgba(201, 183, 156, 0.08), transparent 55%),
radial-gradient(90% 60% at 100% 0%, rgba(255, 255, 255, 0.04), transparent 60%);
}
img {
display: block;
max-width: 100%;
}
button {
font-family: inherit;
}
:focus-visible {
outline: 2px solid var(--sand);
outline-offset: 3px;
}
/* ---------- Masthead ---------- */
.masthead {
position: relative;
z-index: 1;
padding: 56px 24px 12px;
}
.masthead__inner {
max-width: 1120px;
margin: 0 auto;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
margin-bottom: 34px;
}
.brand__mark {
font-size: 1.25rem;
color: var(--sand);
}
.brand__name {
font-weight: 700;
letter-spacing: 0.02em;
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--sand-d);
}
.masthead__title {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(2.4rem, 7vw, 4.4rem);
line-height: 1.02;
margin: 0 0 18px;
letter-spacing: -0.01em;
}
.masthead__lede {
max-width: 46ch;
margin: 0 0 22px;
color: var(--muted);
font-size: 1.02rem;
}
.masthead__meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.pill {
display: inline-flex;
align-items: center;
padding: 6px 14px;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
background: var(--sand);
color: var(--ink);
}
.pill--ghost {
background: transparent;
color: var(--muted);
border: 1px solid var(--line-2);
}
/* ---------- Grid ---------- */
.wrap {
position: relative;
z-index: 1;
max-width: 1120px;
margin: 0 auto;
padding: 40px 24px 72px;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.thumb {
position: relative;
display: block;
width: 100%;
padding: 0;
border: 0;
cursor: pointer;
overflow: hidden;
border-radius: var(--r-md);
background: var(--surface-2);
aspect-ratio: 4 / 5;
color: var(--paper);
text-align: left;
transition: transform 0.5s var(--ease);
}
.thumb--wide {
grid-column: span 2;
aspect-ratio: 16 / 10;
}
.thumb__img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
transform: scale(1.02);
transition: transform 0.7s var(--ease), filter 0.5s var(--ease);
filter: saturate(0.92) contrast(1.02);
}
.thumb::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(10, 10, 12, 0.78) 0%, rgba(10, 10, 12, 0) 46%);
opacity: 0.85;
transition: opacity 0.4s var(--ease);
}
.thumb__meta {
position: absolute;
left: 14px;
right: 14px;
bottom: 13px;
z-index: 2;
transform: translateY(6px);
opacity: 0;
transition: transform 0.4s var(--ease), opacity 0.4s var(--ease);
}
.thumb__label {
display: block;
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--sand);
margin-bottom: 4px;
}
.thumb__title {
font-family: "Fraunces", Georgia, serif;
font-size: 1.05rem;
font-weight: 600;
line-height: 1.2;
}
.thumb__num {
position: absolute;
top: 12px;
left: 12px;
z-index: 2;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.1em;
color: var(--paper);
background: rgba(11, 11, 12, 0.42);
backdrop-filter: blur(6px);
border: 1px solid var(--line);
padding: 3px 9px;
border-radius: 999px;
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.35s var(--ease), transform 0.35s var(--ease);
}
.thumb:hover,
.thumb:focus-visible {
transform: translateY(-4px);
}
.thumb:hover .thumb__img,
.thumb:focus-visible .thumb__img {
transform: scale(1.09);
filter: saturate(1.05) contrast(1.04);
}
.thumb:hover .thumb__meta,
.thumb:focus-visible .thumb__meta,
.thumb:hover .thumb__num,
.thumb:focus-visible .thumb__num {
opacity: 1;
transform: translateY(0);
}
.thumb:active {
transform: translateY(-1px) scale(0.99);
}
/* ---------- Footer ---------- */
.foot {
position: relative;
z-index: 1;
border-top: 1px solid var(--line);
padding: 26px 24px 48px;
text-align: center;
color: var(--muted);
font-size: 0.82rem;
}
.foot p {
margin: 0;
}
/* ---------- Lightbox ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 100;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 24px;
opacity: 0;
transition: opacity 0.32s var(--ease);
}
.lightbox[hidden] {
display: none;
}
.lightbox.is-open {
opacity: 1;
}
.lightbox__backdrop {
position: absolute;
inset: 0;
background: rgba(6, 6, 8, 0.72);
backdrop-filter: blur(18px) saturate(1.1);
-webkit-backdrop-filter: blur(18px) saturate(1.1);
}
.stage {
position: relative;
z-index: 2;
grid-column: 2;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
max-width: min(78vw, 960px);
transform: scale(0.96);
opacity: 0;
transition: transform 0.4s var(--ease), opacity 0.4s var(--ease);
}
.lightbox.is-open .stage {
transform: scale(1);
opacity: 1;
}
.stage__frame {
position: relative;
border-radius: var(--r-md);
overflow: hidden;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: 0 40px 90px rgba(0, 0, 0, 0.55);
max-height: 68vh;
}
.stage__img {
width: auto;
max-width: min(78vw, 960px);
max-height: 68vh;
object-fit: contain;
}
.stage__img.is-swapping {
opacity: 0;
transform: scale(0.985);
}
.stage__img {
transition: opacity 0.28s var(--ease), transform 0.28s var(--ease);
}
.caption {
width: 100%;
max-width: 640px;
text-align: center;
}
.caption__head {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
margin-bottom: 8px;
}
.caption__index {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.16em;
color: var(--sand);
}
.caption__loc {
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--muted);
}
.caption__title {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(1.3rem, 3vw, 1.85rem);
margin: 0 0 6px;
line-height: 1.15;
}
.caption__credit {
margin: 0;
color: var(--muted);
font-size: 0.85rem;
}
/* Lightbox buttons */
.lb-btn {
position: relative;
z-index: 3;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--line-2);
background: rgba(23, 23, 27, 0.66);
color: var(--paper);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
cursor: pointer;
transition: background 0.25s var(--ease), transform 0.2s var(--ease), border-color 0.25s var(--ease);
}
.lb-btn--nav {
width: 56px;
height: 56px;
border-radius: 999px;
font-size: 1.9rem;
line-height: 1;
}
.lb-btn--prev {
grid-column: 1;
justify-self: start;
}
.lb-btn--next {
grid-column: 3;
justify-self: end;
}
.lb-btn:hover {
background: var(--sand);
color: var(--ink);
border-color: var(--sand);
}
.lb-btn:active {
transform: scale(0.93);
}
.lb-btn--close {
position: absolute;
top: 20px;
right: 20px;
z-index: 4;
width: 44px;
height: 44px;
border-radius: var(--r-sm);
font-size: 1rem;
}
.lb-counter {
position: absolute;
bottom: 22px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.2em;
color: var(--paper);
background: rgba(11, 11, 12, 0.5);
border: 1px solid var(--line);
padding: 6px 16px;
border-radius: 999px;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
z-index: 200;
padding: 11px 20px;
border-radius: 999px;
background: var(--paper);
color: var(--ink);
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.01em;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
body.is-locked {
overflow: hidden;
}
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.thumb--wide {
grid-column: span 2;
}
.stage__img {
max-width: 90vw;
}
.stage {
max-width: 90vw;
}
}
@media (max-width: 520px) {
.masthead {
padding: 40px 18px 8px;
}
.brand {
margin-bottom: 24px;
}
.wrap {
padding: 28px 18px 56px;
}
.grid {
grid-template-columns: 1fr;
gap: 12px;
}
.thumb,
.thumb--wide {
grid-column: auto;
aspect-ratio: 4 / 5;
}
.thumb__meta {
opacity: 1;
transform: none;
}
.thumb__num {
opacity: 1;
transform: none;
}
.lightbox {
grid-template-columns: 1fr;
padding: 16px;
align-content: center;
}
.stage {
grid-column: 1;
order: 1;
max-width: 100%;
}
.stage__img {
max-width: 92vw;
max-height: 56vh;
}
.lb-btn--nav {
position: absolute;
top: 50%;
width: 46px;
height: 46px;
font-size: 1.6rem;
transform: translateY(-50%);
}
.lb-btn--prev {
left: 10px;
}
.lb-btn--next {
right: 10px;
}
.lb-btn--nav:active {
transform: translateY(-50%) scale(0.92);
}
.caption {
order: 2;
}
.lb-counter {
bottom: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition-duration: 0.01ms !important;
}
}/* Aperture — Gallery Lightbox
Vanilla JS. Thumbnail grid → full-bleed overlay with prev/next,
keyboard arrows + Esc, focus trap, caption + counter. */
(function () {
"use strict";
// --- Data (fictional, Unsplash placeholder imagery) ---
var PHOTOS = [
{ id: "p1", label: "Coastline", title: "Low Tide, Reykjanes", loc: "Iceland", credit: "Nadia Ostrup · 80mm · f/8", wide: true, q: "photo-1500534623283-312aade485b7" },
{ id: "p2", label: "Desert", title: "Dune Study No. 2", loc: "Namibia", credit: "Marco Vidal · 120mm · f/11", wide: false, q: "photo-1509316785289-025f5b846b35" },
{ id: "p3", label: "City", title: "Rooftop Blue Hour", loc: "Lisbon", credit: "Ana Sørenson · 35mm · f/2.8", wide: false, q: "photo-1513622470522-26c3c8a854bc" },
{ id: "p4", label: "Portrait", title: "Weaver, Hoi An", loc: "Vietnam", credit: "Tomás Reyes · 85mm · f/1.8", wide: false, q: "photo-1544005313-94ddf0286df2" },
{ id: "p5", label: "Coastline", title: "Fog Bank, Big Sur", loc: "California", credit: "Nadia Ostrup · 50mm · f/5.6", wide: true, q: "photo-1470071459604-3b5ec3a7fe05" },
{ id: "p6", label: "Still Life", title: "Silver & Salt", loc: "Studio", credit: "Ana Sørenson · 90mm · f/16", wide: false, q: "photo-1519681393784-d120267933ba" },
{ id: "p7", label: "Mountain", title: "First Light, Dolomites", loc: "Italy", credit: "Marco Vidal · 24mm · f/9", wide: false, q: "photo-1454496522488-7a8e488e8606" },
{ id: "p8", label: "Street", title: "Neon After Rain", loc: "Tokyo", credit: "Kenji Aoki · 35mm · f/2", wide: false, q: "photo-1493514789931-586cb221d7a7" },
{ id: "p9", label: "Forest", title: "Understory Green", loc: "Oregon", credit: "Tomás Reyes · 50mm · f/4", wide: true, q: "photo-1441974231531-c6227db76b6e" },
{ id: "p10", label: "Portrait", title: "The Diver", loc: "Azores", credit: "Ana Sørenson · 85mm · f/2.2", wide: false, q: "photo-1504006833117-8886a355efbf" },
{ id: "p11", label: "Desert", title: "Salt Flat Mirror", loc: "Bolivia", credit: "Marco Vidal · 24mm · f/11", wide: false, q: "photo-1469474968028-56623f02e42e" },
{ id: "p12", label: "City", title: "Cable Car, Steep Street", loc: "Valparaíso", credit: "Kenji Aoki · 40mm · f/5.6", wide: false, q: "photo-1518391846015-55a9cc003b25" }
];
function imgUrl(q, w) {
return "https://images.unsplash.com/" + q + "?auto=format&fit=crop&w=" + w + "&q=80";
}
var TOTAL = PHOTOS.length;
// --- Elements ---
var grid = document.getElementById("grid");
var lb = document.getElementById("lightbox");
var lbImg = document.getElementById("lbImg");
var lbTitle = document.getElementById("lbTitle");
var lbLoc = document.getElementById("lbLoc");
var lbCredit = document.getElementById("lbCredit");
var lbIndex = document.getElementById("lbIndex");
var lbCounter = document.getElementById("lbCounter");
var lbPrev = document.getElementById("lbPrev");
var lbNext = document.getElementById("lbNext");
var lbClose = document.getElementById("lbClose");
var toastEl = document.getElementById("toast");
var current = 0;
var lastFocused = null;
var toastTimer = null;
// --- Toast helper ---
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 1900);
}
// --- Build thumbnail grid ---
function buildGrid() {
var frag = document.createDocumentFragment();
PHOTOS.forEach(function (p, i) {
var btn = document.createElement("button");
btn.type = "button";
btn.className = "thumb" + (p.wide ? " thumb--wide" : "");
btn.setAttribute("role", "listitem");
btn.setAttribute("aria-label", "Open photo " + (i + 1) + " of " + TOTAL + ": " + stripTags(p.title));
btn.dataset.index = String(i);
var num = document.createElement("span");
num.className = "thumb__num";
num.textContent = pad(i + 1) + " / " + pad(TOTAL);
var img = document.createElement("img");
img.className = "thumb__img";
img.loading = "lazy";
img.alt = stripTags(p.title) + " — " + p.loc;
img.src = imgUrl(p.q, p.wide ? 900 : 640);
var meta = document.createElement("div");
meta.className = "thumb__meta";
meta.innerHTML =
'<span class="thumb__label">' + p.label + "</span>" +
'<span class="thumb__title">' + p.title + "</span>";
btn.appendChild(num);
btn.appendChild(img);
btn.appendChild(meta);
btn.addEventListener("click", function () {
open(i, btn);
});
frag.appendChild(btn);
});
grid.appendChild(frag);
}
function pad(n) {
return n < 10 ? "0" + n : String(n);
}
function stripTags(s) {
var d = document.createElement("div");
d.innerHTML = s;
return d.textContent || "";
}
// --- Render current photo into stage ---
function render(animate) {
var p = PHOTOS[current];
var counter = current + 1 + " / " + TOTAL;
function apply() {
lbImg.src = imgUrl(p.q, 1400);
lbImg.alt = stripTags(p.title) + " — " + p.loc;
lbTitle.innerHTML = p.title;
lbLoc.textContent = p.loc;
lbCredit.textContent = p.credit;
lbIndex.textContent = counter;
lbCounter.textContent = counter;
}
if (animate) {
lbImg.classList.add("is-swapping");
setTimeout(function () {
apply();
// let the browser paint the new src before revealing
requestAnimationFrame(function () {
lbImg.classList.remove("is-swapping");
});
}, 140);
} else {
apply();
}
}
// --- Open / close ---
function open(index, trigger) {
current = clamp(index);
lastFocused = trigger || document.activeElement;
lb.hidden = false;
lb.setAttribute("aria-hidden", "false");
document.body.classList.add("is-locked");
render(false);
// next frame → animate in
requestAnimationFrame(function () {
lb.classList.add("is-open");
});
lbNext.focus();
document.addEventListener("keydown", onKey);
}
function close() {
lb.classList.remove("is-open");
lb.setAttribute("aria-hidden", "true");
document.body.classList.remove("is-locked");
document.removeEventListener("keydown", onKey);
setTimeout(function () {
lb.hidden = true;
}, 320);
if (lastFocused && typeof lastFocused.focus === "function") {
lastFocused.focus();
}
}
function clamp(i) {
return (i % TOTAL + TOTAL) % TOTAL; // wrap both directions
}
function go(delta) {
current = clamp(current + delta);
render(true);
}
// --- Keyboard handling (arrows, Esc, focus trap) ---
function onKey(e) {
switch (e.key) {
case "Escape":
e.preventDefault();
close();
toast("Viewer closed");
break;
case "ArrowRight":
e.preventDefault();
go(1);
break;
case "ArrowLeft":
e.preventDefault();
go(-1);
break;
case "Home":
e.preventDefault();
current = 0;
render(true);
break;
case "End":
e.preventDefault();
current = TOTAL - 1;
render(true);
break;
case "Tab":
trapFocus(e);
break;
}
}
function trapFocus(e) {
var focusables = [lbClose, lbPrev, lbNext];
var first = focusables[0];
var last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
} else if (focusables.indexOf(document.activeElement) === -1) {
e.preventDefault();
first.focus();
}
}
// --- Wire controls ---
lbPrev.addEventListener("click", function () { go(-1); });
lbNext.addEventListener("click", function () { go(1); });
lbClose.addEventListener("click", function () {
close();
toast("Viewer closed");
});
Array.prototype.forEach.call(document.querySelectorAll("[data-close]"), function (el) {
el.addEventListener("click", close);
});
// Basic swipe support on the stage
(function enableSwipe() {
var stage = document.getElementById("stage");
var startX = 0;
var active = false;
stage.addEventListener("touchstart", function (e) {
startX = e.changedTouches[0].clientX;
active = true;
}, { passive: true });
stage.addEventListener("touchend", function (e) {
if (!active) return;
active = false;
var dx = e.changedTouches[0].clientX - startX;
if (Math.abs(dx) > 44) go(dx < 0 ? 1 : -1);
}, { passive: true });
})();
// --- Init ---
buildGrid();
setTimeout(function () {
toast("Tap a frame to open the lightbox");
}, 700);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aperture — Gallery Lightbox</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fraunces:wght@400;600&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="grain" aria-hidden="true"></div>
<header class="masthead">
<div class="masthead__inner">
<div class="brand">
<span class="brand__mark" aria-hidden="true">◐</span>
<span class="brand__name">Aperture</span>
</div>
<p class="eyebrow">Field Journal — No. 04</p>
<h1 class="masthead__title">Salt & Silver</h1>
<p class="masthead__lede">
A roaming series shot on medium format across coastlines, quiet cities and
high desert. Tap any frame to open the lightbox — arrow keys to travel, Esc to leave.
</p>
<div class="masthead__meta">
<span class="pill">12 frames</span>
<span class="pill pill--ghost">Medium format</span>
<span class="pill pill--ghost">2024 — 2026</span>
</div>
</div>
</header>
<main class="wrap">
<div class="grid" id="grid" role="list" aria-label="Photograph thumbnails">
<!-- Thumbnails injected by script.js -->
</div>
</main>
<footer class="foot">
<p>Aperture Studio — fictional portfolio. Imagery via Unsplash placeholders.</p>
</footer>
<!-- Lightbox overlay -->
<div
class="lightbox"
id="lightbox"
role="dialog"
aria-modal="true"
aria-label="Photograph viewer"
aria-hidden="true"
hidden
>
<div class="lightbox__backdrop" data-close></div>
<button class="lb-btn lb-btn--close" id="lbClose" type="button" aria-label="Close viewer (Esc)">
<span aria-hidden="true">✕</span>
</button>
<button class="lb-btn lb-btn--nav lb-btn--prev" id="lbPrev" type="button" aria-label="Previous photo (Left arrow)">
<span aria-hidden="true">‹</span>
</button>
<figure class="stage" id="stage">
<div class="stage__frame">
<img class="stage__img" id="lbImg" src="" alt="" />
</div>
<figcaption class="caption" id="lbCaption">
<div class="caption__head">
<span class="caption__index" id="lbIndex">1 / 12</span>
<span class="caption__loc" id="lbLoc"></span>
</div>
<h2 class="caption__title" id="lbTitle"></h2>
<p class="caption__credit" id="lbCredit"></p>
</figcaption>
</figure>
<button class="lb-btn lb-btn--nav lb-btn--next" id="lbNext" type="button" aria-label="Next photo (Right arrow)">
<span aria-hidden="true">›</span>
</button>
<div class="lb-counter" id="lbCounter" aria-hidden="true">1 / 12</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A reusable, dependency-free lightbox built for photographers and editorial galleries. The landing view is a masonry-leaning thumbnail grid where each frame is a full-bleed image with a thin uppercase label and a subtle zoom-on-hover. Everything sits on a deep gallery-dark surface so the photography does the talking, with generous whitespace and a display serif for the headings.
Click any thumbnail — or focus it and press Enter — and the lightbox lifts into view over a blurred, dimmed backdrop. The overlay shows the image large and centered, a caption strip with the photo title, location and photographer credit, and a live counter reading the current position such as 3 / 12. Left and right arrow buttons page through the set; wrapping is enabled so you loop cleanly from the last frame back to the first.
Keyboard support is first-class: arrow keys navigate, Esc closes, and Tab is trapped inside the overlay so focus never escapes to the page behind it. Opening the lightbox locks body scroll and remembers the thumbnail you came from, restoring focus to it on close. A tiny toast helper confirms actions, and the whole thing degrades gracefully down to a single-column grid at ~360px.