<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="lb-i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></symbol>
<symbol id="lb-i-prev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5.5 8 12l6.5 6.5"/></symbol>
<symbol id="lb-i-next" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5.5 16 12l-6.5 6.5"/></symbol>
<symbol id="lb-i-play" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.8v14.4a1 1 0 0 0 1.5.86l12-7.2a1 1 0 0 0 0-1.72l-12-7.2A1 1 0 0 0 7 4.8z"/></symbol>
<symbol id="lb-i-star" viewBox="0 0 20 20" fill="currentColor"><path d="M10 1.8l2.5 5.2 5.7.8-4.1 4 1 5.7L10 14.8l-5.1 2.7 1-5.7-4.1-4 5.7-.8z"/></symbol>
</svg>
<div><span class="lbl">Image lightbox with captions</span>
<div class="lb-grid">
<button class="lb-thumb" type="button" data-open="lb-lightbox" aria-haspopup="dialog" aria-label="Open photo 1 of 5: Sea of cloud at last light"
data-src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="Snow-capped peaks rising above a sea of cloud at sunset"
data-title="Sea of cloud at last light" data-place="High alpine ridge" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=480&h=320&q=70" width="480" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-open="lb-lightbox" aria-haspopup="dialog" aria-label="Open photo 2 of 5: Mist over the escarpment"
data-src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="Cloud spilling over a green escarpment at sunrise, a road winding through the valley below"
data-title="Mist over the escarpment" data-place="Highland plateau at sunrise" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=480&h=320&q=70" width="480" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-open="lb-lightbox" aria-haspopup="dialog" aria-label="Open photo 3 of 5: Glacier valley in summer"
data-src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="A wide glacial valley between snowy mountains, pine trees in the foreground"
data-title="Glacier valley in summer" data-place="Northern mountain range" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=320&h=320&q=70" width="320" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-open="lb-lightbox" aria-haspopup="dialog" aria-label="Open photo 4 of 5: Road through red rock"
data-src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="An empty two-lane road cutting through red sandstone towards a grey peak"
data-title="Road through red rock" data-place="Desert canyon highway" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=320&h=320&q=70" width="320" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-open="lb-lightbox" aria-haspopup="dialog" aria-label="Open photo 5 of 5: Aurora over the pines"
data-src="https://images.unsplash.com/photo-1483347756197-71ef80e95f73?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="Green and violet northern lights above a silhouetted pine forest"
data-title="Aurora over the pines" data-place="Boreal forest, winter night" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1483347756197-71ef80e95f73?auto=format&fit=crop&w=320&h=320&q=70" width="320" height="320" alt=""></button>
</div></div>
<div><span class="lbl">Video</span>
<button class="lb-poster" type="button" data-open="lb-video" aria-haspopup="dialog" aria-label="Watch the clip: Bloom, 5 seconds">
<img src="https://images.unsplash.com/photo-1496062031456-07b8f162a322?auto=format&fit=crop&w=660&h=372&q=70" width="660" height="372" alt="">
<span class="lb-play" aria-hidden="true"><svg><use href="#lb-i-play"/></svg></span>
<span class="lb-poster-text"><b>Watch the clip</b><span>Bloom · a CC0 time-lapse</span></span>
<span class="lb-chip">0:05</span>
</button></div>
<div><span class="lbl">Product quick view</span>
<div class="lb-product">
<img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=200&h=200&q=70" width="84" height="84" alt="Alder lounge chair in mustard">
<div><p class="lb-product-name">Alder lounge chair</p><p class="lb-product-price">£349</p>
<button class="lb-btn" type="button" data-open="lb-quick" aria-haspopup="dialog" aria-label="Quick view: Alder lounge chair">Quick view</button></div>
</div></div>
<!-- 1. Image lightbox: one dialog, filled from the data-* attributes of the thumbnail that opened it -->
<dialog class="lb is-photo" id="lb-lightbox" aria-labelledby="lb-photo-title">
<form class="lb-form" method="dialog">
<figure class="lb-figure">
<div class="lb-media">
<img data-lb-img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1500&h=1000&q=70" width="1500" height="1000" alt="Snow-capped peaks rising above a sea of cloud at sunset">
<button class="lb-nav is-prev" type="button" data-lb-prev aria-label="Previous photo"><svg aria-hidden="true"><use href="#lb-i-prev"/></svg></button>
<button class="lb-nav is-next" type="button" data-lb-next aria-label="Next photo"><svg aria-hidden="true"><use href="#lb-i-next"/></svg></button>
</div>
<figcaption class="lb-cap">
<div class="lb-cap-text" aria-live="polite">
<h2 class="lb-title" id="lb-photo-title" data-lb-title>Sea of cloud at last light</h2>
<p class="lb-meta"><span data-lb-place>High alpine ridge</span> · <span data-lb-credit>Photo: Unsplash</span></p>
</div>
<span class="lb-count" data-lb-count>1 / 5</span>
<button class="lb-x" value="close" aria-label="Close"><svg aria-hidden="true"><use href="#lb-i-x"/></svg></button>
</figcaption>
</figure>
</form>
</dialog>
<!-- 2. Video: preload="none" fetches nothing until play; the script pauses it when the dialog closes -->
<dialog class="lb is-video" id="lb-video" aria-labelledby="lb-video-title">
<form class="lb-form" method="dialog">
<div class="lb-player">
<video controls playsinline preload="none" poster="https://images.unsplash.com/photo-1496062031456-07b8f162a322?auto=format&fit=crop&w=1280&h=720&q=70" width="1280" height="720">
<source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4">
<track kind="captions" srclang="en" label="English" src="data:text/vtt;charset=utf-8,WEBVTT%0A%0A00:00.000 --> 00:05.000%0A[No dialogue. Time-lapse of a red flower bud opening.]">
</video>
</div>
<div class="lb-cap">
<div class="lb-cap-text">
<h2 class="lb-title" id="lb-video-title">Bloom: a 5-second CC0 clip</h2>
<p class="lb-meta">Time-lapse of a red flower bud opening · CC0 sample video from MDN</p>
</div>
<button class="lb-x" value="close" aria-label="Close"><svg aria-hidden="true"><use href="#lb-i-x"/></svg></button>
</div>
</form>
</dialog>
<!-- 3. Product quick view -->
<dialog class="lb is-quick" id="lb-quick" aria-labelledby="lb-quick-title">
<form class="lb-form" method="dialog">
<button class="lb-x is-float" value="close" aria-label="Close"><svg aria-hidden="true"><use href="#lb-i-x"/></svg></button>
<div class="lb-qv" tabindex="0" role="region" aria-label="Alder lounge chair details">
<div class="lb-qv-media"><img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=880&h=960&q=70" width="880" height="960" alt="Alder lounge chair in mustard beside a brass floor lamp"></div>
<div class="lb-qv-body">
<p class="lb-kicker">Alder collection</p>
<h2 class="lb-qv-title" id="lb-quick-title">Alder lounge chair</h2>
<p class="lb-price">£349</p>
<p class="lb-rating"><span class="lb-stars" aria-hidden="true"><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg></span>4.7 out of 5 · 212 reviews</p>
<p class="lb-desc">A low, deep seat on a solid oak frame, upholstered in a hard-wearing wool blend. Made in Nottingham and delivered fully assembled.</p>
<fieldset class="lb-field">
<legend class="lb-label">Colour</legend>
<div class="lb-swatches">
<label class="lb-swatch"><input class="lb-sr" type="radio" name="lb-colour" value="mustard" checked><span style="--swatch:#d9a21b">Mustard</span></label>
<label class="lb-swatch"><input class="lb-sr" type="radio" name="lb-colour" value="sage"><span style="--swatch:#9caf88">Sage</span></label>
<label class="lb-swatch"><input class="lb-sr" type="radio" name="lb-colour" value="charcoal"><span style="--swatch:#3f3f46">Charcoal</span></label>
<label class="lb-swatch"><input class="lb-sr" type="radio" name="lb-colour" value="oat"><span style="--swatch:#e7dcc8">Oat</span></label>
</div>
</fieldset>
<div class="lb-buy">
<div><label class="lb-label" for="lb-qty">Quantity</label>
<select class="lb-select" id="lb-qty" name="qty"><option>1</option><option>2</option><option>3</option><option>4</option></select></div>
<button class="lb-btn is-primary" value="add">Add to basket</button>
</div>
<div class="lb-more"><a class="lb-link" href="#">View full details</a><span class="lb-note">Free delivery in 5–7 days</span></div>
</div>
</div>
</form>
</dialog>
CSS
/* Media modals on the native <dialog> element: an image lightbox, a video player and a product quick view.
showModal() puts each dialog in the top layer, keeps focus inside it, closes it on Esc and makes the page inert. */
.lb-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* Triggers on the (light) page */
.lb-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px}
.lb-thumb{grid-column:span 2;display:block;aspect-ratio:1;padding:0;border:0;border-radius:10px;background:#0b0b0f;overflow:hidden;cursor:zoom-in}
.lb-thumb:nth-child(-n+2){grid-column:span 3;aspect-ratio:3/2}
.lb-thumb img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .35s ease,opacity .2s}
.lb-thumb:hover img{transform:scale(1.05);opacity:.9}
.lb-thumb:focus-visible,.lb-poster:focus-visible{outline:3px solid #b45309;outline-offset:3px}
.lb-poster{position:relative;display:block;width:100%;aspect-ratio:16/9;padding:0;border:0;border-radius:14px;background:#0b0b0f;color:#fff;font-family:inherit;text-align:left;overflow:hidden;cursor:pointer}
.lb-poster img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .35s ease}
.lb-poster:hover img{transform:scale(1.04)}
.lb-poster::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(11,11,15,.9),rgba(11,11,15,.1) 60%)}
.lb-play{position:absolute;z-index:1;left:50%;top:44%;display:grid;place-items:center;width:58px;height:58px;border-radius:50%;background:rgba(255,255,255,.94);color:#0b0b0f;
transform:translate(-50%,-50%);box-shadow:0 10px 30px -8px rgba(0,0,0,.6);transition:transform .2s}
.lb-poster:hover .lb-play{transform:translate(-50%,-50%) scale(1.08)}
.lb-play svg{width:22px;height:22px;margin-left:3px}
.lb-poster-text{position:absolute;z-index:1;left:16px;right:74px;bottom:13px;line-height:1.3}
.lb-poster-text b{display:block;font-size:.95rem}
.lb-poster-text span{display:block;margin-top:2px;color:#e4e4e7;font-size:.78rem}
.lb-chip{position:absolute;z-index:1;right:14px;bottom:15px;padding:2px 8px;border-radius:999px;background:rgba(11,11,15,.8);color:#fff;font:600 .75rem/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.lb-product{display:flex;align-items:center;gap:14px;padding:12px;border:1px solid #e2e8f0;border-radius:14px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.lb-product img{flex:none;width:84px;height:84px;border-radius:10px;object-fit:cover}
.lb-product-name{margin:0;color:#0f172a;font-size:.95rem;font-weight:600;line-height:1.3}
.lb-product-price{margin:2px 0 10px;color:#475569;font-size:.875rem}
.lb-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px;padding:8px 16px;border:1px solid #cbd5e1;border-radius:10px;background:#fff;color:#0f172a;
font-family:inherit;font-size:.875rem;font-weight:600;line-height:1.2;text-decoration:none;cursor:pointer;transition:background-color .15s,border-color .15s}
.lb-btn:hover{background:#f1f5f9;border-color:#94a3b8}
.lb-btn:focus-visible{outline:2px solid #b45309;outline-offset:2px}
.lb-btn.is-primary{min-height:46px;border-color:#f59e0b;background:#f59e0b;color:#0b0b0f}
.lb-btn.is-primary:hover{border-color:#d97706;background:#d97706}
/* The dialogs */
.lb{--lb-accent:#f59e0b;--lb-bg:#0b0b0f;--lb-ink:#fafafa;--lb-muted:#a1a1aa;--lb-line:rgba(255,255,255,.14);--lb-w:880px;
width:min(var(--lb-w),calc(100% - 32px));max-width:none;max-height:calc(100dvh - 32px);margin:auto;padding:0;border:0;border-radius:16px;
background:var(--lb-bg);color:var(--lb-ink);font-size:.95rem;line-height:1.5;overflow:hidden;overscroll-behavior:contain;
box-shadow:0 0 0 1px rgba(255,255,255,.07),0 40px 90px -20px rgba(0,0,0,.85)}
.lb[open]{display:flex;flex-direction:column;animation:lb-in .25s cubic-bezier(.2,.8,.2,1)}
.lb::backdrop{background:rgba(5,5,8,.88)}
.lb[open]::backdrop{animation:lb-fade .25s ease-out}
@keyframes lb-in{from{opacity:0;transform:scale(.96)}}
@keyframes lb-fade{from{opacity:0}}
html:has(.lb:modal){overflow:hidden} /* stop the page scrolling behind an open modal */
.lb-form{position:relative;display:flex;flex-direction:column;flex:1;min-height:0;margin:0}
/* Caption row under the photo or video: title and meta, counter, close */
.lb-cap{display:flex;align-items:center;gap:14px;margin:0;padding:16px 18px 18px 22px}
.lb-cap-text{flex:1;min-width:0}
.lb-title{margin:0;font-size:1.05rem;font-weight:650;letter-spacing:-.01em;line-height:1.35}
.lb-meta{margin:2px 0 0;color:var(--lb-muted);font-size:.85rem}
.lb-count{flex:none;padding:3px 10px;border:1px solid rgba(245,158,11,.45);border-radius:999px;color:var(--lb-accent);font-size:.8rem;font-weight:600;line-height:1.4;font-variant-numeric:tabular-nums;white-space:nowrap}
.lb-x{flex:none;display:grid;place-items:center;width:40px;height:40px;padding:0;border:1px solid var(--lb-line);border-radius:50%;background:transparent;color:#e4e4e7;cursor:pointer;
transition:background-color .15s,color .15s}
.lb-x:hover{background:rgba(255,255,255,.1);color:#fff}
.lb-x svg{width:18px;height:18px}
.lb-x:focus-visible,.lb-nav:focus-visible{outline:2px solid var(--lb-accent);outline-offset:2px}
/* 1. Lightbox: the photo box keeps a 3:2 ratio (no jump between photos) and shrinks to fit short screens */
.lb.is-photo{--lb-w:1040px;width:min(var(--lb-w),calc(100% - 32px),calc((100dvh - 150px) * 1.5))}
.lb-figure{margin:0}
.lb-media{position:relative;aspect-ratio:3/2;background:#000}
.lb-media img{display:block;width:100%;height:100%;object-fit:contain}
.lb-nav{position:absolute;top:50%;display:grid;place-items:center;width:46px;height:46px;padding:0;border:1px solid rgba(255,255,255,.25);border-radius:50%;
background:rgba(11,11,15,.62);color:#fff;cursor:pointer;transform:translateY(-50%);transition:background-color .15s,border-color .15s}
.lb-nav:hover{background:rgba(11,11,15,.88);border-color:rgba(255,255,255,.5)}
.lb-nav svg{width:22px;height:22px}
.lb-nav.is-prev{left:14px}
.lb-nav.is-next{right:14px}
/* 2. Video */
.lb-player{aspect-ratio:16/9;background:#000}
.lb-player video{display:block;width:100%;height:100%;background:#000}
.lb-player video:focus-visible{outline:2px solid var(--lb-accent);outline-offset:-2px}
/* 3. Product quick view: a white panel, two columns that stack on phones */
.lb.is-quick{--lb-w:920px;--lb-accent:#b45309;--lb-ink:#0f172a;--lb-muted:#475569;--lb-line:#e2e8f0;background:#fff;box-shadow:0 0 0 1px rgba(15,23,42,.06),0 40px 90px -20px rgba(0,0,0,.7)}
.lb.is-quick .lb-x.is-float{position:absolute;z-index:1;top:12px;right:12px;border-color:#e2e8f0;background:#fff;color:#334155;box-shadow:0 4px 12px -4px rgba(15,23,42,.25)}
.lb.is-quick .lb-x.is-float:hover{background:#f1f5f9;color:#0f172a}
.lb-qv{display:grid;grid-template-columns:1fr 1fr;min-height:0;overflow-y:auto;overscroll-behavior:contain}
.lb-qv:focus-visible{outline:2px solid var(--lb-accent);outline-offset:-2px}
.lb-qv-media{background:#f4f1ec}
.lb-qv-media img{display:block;width:100%;height:100%;min-height:360px;object-fit:cover}
.lb-qv-body{padding:34px 30px 28px}
.lb-kicker{margin:0 0 6px;color:#b45309;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
.lb-qv-title{margin:0;padding-right:40px;font-size:1.45rem;font-weight:700;letter-spacing:-.015em;line-height:1.25}
.lb-price{margin:8px 0 0;font-size:1.3rem;font-weight:700}
.lb-rating{display:flex;align-items:center;gap:8px;margin:6px 0 0;color:#475569;font-size:.85rem}
.lb-stars{display:flex;gap:1px;color:#f59e0b}
.lb-stars svg{width:15px;height:15px}
.lb-desc{margin:14px 0 0;color:#475569;font-size:.9rem}
.lb-field{margin:18px 0 0;padding:0;border:0;min-width:0}
.lb-label{display:block;margin:0 0 8px;padding:0;color:#0f172a;font-size:.8rem;font-weight:600}
.lb-swatches{display:flex;flex-wrap:wrap;gap:6px}
.lb-swatch{position:relative;cursor:pointer}
.lb-swatch span{display:inline-flex;align-items:center;gap:7px;padding:6px 12px 6px 6px;border:1px solid #cbd5e1;border-radius:999px;color:#334155;font-size:.85rem;line-height:1.2;
transition:border-color .15s,box-shadow .15s}
.lb-swatch span::before{content:"";width:18px;height:18px;border-radius:50%;background:var(--swatch);box-shadow:inset 0 0 0 1px rgba(15,23,42,.18)}
.lb-swatch:hover span{border-color:#64748b}
.lb-swatch input:checked+span{border-color:#0f172a;box-shadow:inset 0 0 0 1px #0f172a;color:#0f172a;font-weight:600}
.lb-swatch input:focus-visible+span{outline:2px solid #b45309;outline-offset:2px}
.lb-buy{display:flex;align-items:flex-end;gap:10px;margin-top:20px}
.lb-select{min-width:84px;min-height:46px;padding:0 34px 0 14px;border:1px solid #94a3b8;border-radius:10px;
background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23334155' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E") no-repeat right 12px center/14px;
color:#0f172a;font:inherit;font-size:.9rem;appearance:none;cursor:pointer}
.lb-select:focus-visible{outline:2px solid #b45309;outline-offset:2px}
.lb-buy .lb-btn{flex:1}
.lb-more{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:6px 16px;margin-top:16px;font-size:.85rem}
.lb-link{color:#0f172a;font-weight:600;text-decoration:underline;text-underline-offset:3px}
.lb-link:hover{color:#b45309}
.lb-link:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
.lb-note{color:#475569}
@media (max-width:767.98px){
.lb-qv{grid-template-columns:1fr}
.lb-qv-media img{height:240px;min-height:0}
.lb-qv-body{padding:22px 20px 24px}
}
@media (max-width:600px){
.lb-cap{gap:10px;padding:12px 12px 14px 16px}
.lb-title{font-size:.95rem}
.lb-meta{font-size:.8rem}
.lb-nav{width:40px;height:40px}
.lb-nav.is-prev{left:8px}
.lb-nav.is-next{right:8px}
}
@media (prefers-reduced-motion:reduce){
.lb[open],.lb[open]::backdrop{animation:none}
.lb-thumb img,.lb-poster img,.lb-play,.lb-btn,.lb-x,.lb-nav,.lb-swatch span{transition:none}
.lb-thumb:hover img,.lb-poster:hover img{transform:none}
.lb-poster:hover .lb-play{transform:translate(-50%,-50%)}
}
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script.
document.querySelectorAll('[data-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
});
// A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});
// Lightbox: every thumbnail carries its photo in data-* attributes; one dialog shows whichever is current.
(function () {
var box = document.getElementById('lb-lightbox');
if (!box) return;
var thumbs = Array.prototype.slice.call(document.querySelectorAll('[data-open="lb-lightbox"]'));
var img = box.querySelector('[data-lb-img]'), current = 0;
function show(i) {
current = (i + thumbs.length) % thumbs.length; // wrap around at both ends
var d = thumbs[current].dataset;
img.src = d.src; img.alt = d.alt;
box.querySelector('[data-lb-title]').textContent = d.title;
box.querySelector('[data-lb-place]').textContent = d.place;
box.querySelector('[data-lb-credit]').textContent = d.credit;
box.querySelector('[data-lb-count]').textContent = (current + 1) + ' / ' + thumbs.length;
new Image().src = thumbs[(current + 1) % thumbs.length].dataset.src; // warm the next photo
}
thumbs.forEach(function (t, i) { t.addEventListener('click', function () { show(i); }); });
box.querySelector('[data-lb-prev]').addEventListener('click', function () { show(current - 1); });
box.querySelector('[data-lb-next]').addEventListener('click', function () { show(current + 1); });
box.addEventListener('keydown', function (e) { // Left and Right arrows step through the photos
if (e.key === 'ArrowLeft') { e.preventDefault(); show(current - 1); }
if (e.key === 'ArrowRight') { e.preventDefault(); show(current + 1); }
});
})();
// Video: stop playback when its dialog closes (Esc, close button or backdrop)
document.querySelectorAll('dialog').forEach(function (dialog) {
var video = dialog.querySelector('video');
if (video) dialog.addEventListener('close', function () { video.pause(); });
});
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="lb-i-prev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5.5 8 12l6.5 6.5"/></symbol>
<symbol id="lb-i-next" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.5 5.5 16 12l-6.5 6.5"/></symbol>
<symbol id="lb-i-play" viewBox="0 0 24 24" fill="currentColor"><path d="M7 4.8v14.4a1 1 0 0 0 1.5.86l12-7.2a1 1 0 0 0 0-1.72l-12-7.2A1 1 0 0 0 7 4.8z"/></symbol>
<symbol id="lb-i-star" viewBox="0 0 20 20" fill="currentColor"><path d="M10 1.8l2.5 5.2 5.7.8-4.1 4 1 5.7L10 14.8l-5.1 2.7 1-5.7-4.1-4 5.7-.8z"/></symbol>
</svg>
<div><span class="lbl">Image lightbox with captions</span>
<div class="lb-grid">
<button class="lb-thumb" type="button" data-bs-toggle="modal" data-bs-target="#lb-lightbox" aria-label="Open photo 1 of 5: Sea of cloud at last light"
data-src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="Snow-capped peaks rising above a sea of cloud at sunset"
data-title="Sea of cloud at last light" data-place="High alpine ridge" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=480&h=320&q=70" width="480" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-bs-toggle="modal" data-bs-target="#lb-lightbox" aria-label="Open photo 2 of 5: Mist over the escarpment"
data-src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="Cloud spilling over a green escarpment at sunrise, a road winding through the valley below"
data-title="Mist over the escarpment" data-place="Highland plateau at sunrise" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=480&h=320&q=70" width="480" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-bs-toggle="modal" data-bs-target="#lb-lightbox" aria-label="Open photo 3 of 5: Glacier valley in summer"
data-src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="A wide glacial valley between snowy mountains, pine trees in the foreground"
data-title="Glacier valley in summer" data-place="Northern mountain range" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=320&h=320&q=70" width="320" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-bs-toggle="modal" data-bs-target="#lb-lightbox" aria-label="Open photo 4 of 5: Road through red rock"
data-src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="An empty two-lane road cutting through red sandstone towards a grey peak"
data-title="Road through red rock" data-place="Desert canyon highway" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=320&h=320&q=70" width="320" height="320" alt=""></button>
<button class="lb-thumb" type="button" data-bs-toggle="modal" data-bs-target="#lb-lightbox" aria-label="Open photo 5 of 5: Aurora over the pines"
data-src="https://images.unsplash.com/photo-1483347756197-71ef80e95f73?auto=format&fit=crop&w=1500&h=1000&q=70" data-alt="Green and violet northern lights above a silhouetted pine forest"
data-title="Aurora over the pines" data-place="Boreal forest, winter night" data-credit="Photo: Unsplash">
<img src="https://images.unsplash.com/photo-1483347756197-71ef80e95f73?auto=format&fit=crop&w=320&h=320&q=70" width="320" height="320" alt=""></button>
</div></div>
<div><span class="lbl">Video</span>
<button class="lb-poster" type="button" data-bs-toggle="modal" data-bs-target="#lb-video" aria-label="Watch the clip: Bloom, 5 seconds">
<img src="https://images.unsplash.com/photo-1496062031456-07b8f162a322?auto=format&fit=crop&w=660&h=372&q=70" width="660" height="372" alt="">
<span class="lb-play" aria-hidden="true"><svg><use href="#lb-i-play"/></svg></span>
<span class="lb-poster-text"><b>Watch the clip</b><span>Bloom · a CC0 time-lapse</span></span>
<span class="badge rounded-pill lb-chip">0:05</span>
</button></div>
<div><span class="lbl">Product quick view</span>
<div class="card lb-product flex-row align-items-center">
<img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=200&h=200&q=70" width="84" height="84" alt="Alder lounge chair in mustard">
<div><p class="lb-product-name">Alder lounge chair</p><p class="lb-product-price">£349</p>
<button class="btn lb-btn" type="button" data-bs-toggle="modal" data-bs-target="#lb-quick" aria-label="Quick view: Alder lounge chair">Quick view</button></div>
</div></div>
<!-- 1. Image lightbox: one modal, filled on show.bs.modal from the thumbnail that opened it (event.relatedTarget) -->
<div class="modal fade lb lb-photo" id="lb-lightbox" tabindex="-1" aria-labelledby="lb-photo-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content">
<figure class="m-0">
<div class="lb-media">
<img data-lb-img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1500&h=1000&q=70" width="1500" height="1000" alt="Snow-capped peaks rising above a sea of cloud at sunset">
<button class="btn rounded-circle position-absolute top-50 translate-middle-y lb-nav is-prev" type="button" data-lb-prev aria-label="Previous photo"><svg aria-hidden="true"><use href="#lb-i-prev"/></svg></button>
<button class="btn rounded-circle position-absolute top-50 translate-middle-y lb-nav is-next" type="button" data-lb-next aria-label="Next photo"><svg aria-hidden="true"><use href="#lb-i-next"/></svg></button>
</div>
<figcaption class="lb-cap">
<div class="lb-cap-text" aria-live="polite">
<h2 class="lb-title" id="lb-photo-title" data-lb-title>Sea of cloud at last light</h2>
<p class="lb-meta"><span data-lb-place>High alpine ridge</span> · <span data-lb-credit>Photo: Unsplash</span></p>
</div>
<span class="badge rounded-pill lb-count" data-lb-count>1 / 5</span>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</figcaption>
</figure>
</div>
</div>
</div>
<!-- 2. Video: preload="none" fetches nothing until play; the script pauses it on hide.bs.modal -->
<div class="modal fade lb lb-video" id="lb-video" tabindex="-1" aria-labelledby="lb-video-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="ratio ratio-16x9 lb-player">
<video controls playsinline preload="none" poster="https://images.unsplash.com/photo-1496062031456-07b8f162a322?auto=format&fit=crop&w=1280&h=720&q=70" width="1280" height="720">
<source src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" type="video/mp4">
<track kind="captions" srclang="en" label="English" src="data:text/vtt;charset=utf-8,WEBVTT%0A%0A00:00.000 --> 00:05.000%0A[No dialogue. Time-lapse of a red flower bud opening.]">
</video>
</div>
<div class="lb-cap">
<div class="lb-cap-text">
<h2 class="lb-title" id="lb-video-title">Bloom: a 5-second CC0 clip</h2>
<p class="lb-meta">Time-lapse of a red flower bud opening · CC0 sample video from MDN</p>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-- 3. Product quick view -->
<div class="modal fade lb lb-quick" id="lb-quick" tabindex="-1" aria-labelledby="lb-quick-title" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
<div class="modal-content">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<div class="modal-body" tabindex="0" role="region" aria-label="Alder lounge chair details">
<div class="row g-0">
<div class="col-md-6 lb-qv-media"><img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=880&h=960&q=70" width="880" height="960" alt="Alder lounge chair in mustard beside a brass floor lamp"></div>
<div class="col-md-6 lb-qv-body">
<p class="lb-kicker">Alder collection</p>
<h2 class="lb-qv-title" id="lb-quick-title">Alder lounge chair</h2>
<p class="lb-price">£349</p>
<p class="lb-rating"><span class="lb-stars" aria-hidden="true"><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg><svg><use href="#lb-i-star"/></svg></span>4.7 out of 5 · 212 reviews</p>
<p class="lb-desc">A low, deep seat on a solid oak frame, upholstered in a hard-wearing wool blend. Made in Nottingham and delivered fully assembled.</p>
<fieldset class="lb-field">
<legend class="lb-label">Colour</legend>
<div class="d-flex flex-wrap lb-swatches">
<input type="radio" class="btn-check" name="lb-colour" id="lb-c-mustard" value="mustard" autocomplete="off" checked><label class="btn lb-swatch" for="lb-c-mustard" style="--swatch:#d9a21b">Mustard</label>
<input type="radio" class="btn-check" name="lb-colour" id="lb-c-sage" value="sage" autocomplete="off"><label class="btn lb-swatch" for="lb-c-sage" style="--swatch:#9caf88">Sage</label>
<input type="radio" class="btn-check" name="lb-colour" id="lb-c-charcoal" value="charcoal" autocomplete="off"><label class="btn lb-swatch" for="lb-c-charcoal" style="--swatch:#3f3f46">Charcoal</label>
<input type="radio" class="btn-check" name="lb-colour" id="lb-c-oat" value="oat" autocomplete="off"><label class="btn lb-swatch" for="lb-c-oat" style="--swatch:#e7dcc8">Oat</label>
</div>
</fieldset>
<div class="d-flex align-items-end gap-2 lb-buy">
<div><label class="form-label lb-label" for="lb-qty">Quantity</label>
<select class="form-select lb-select" id="lb-qty" name="qty"><option>1</option><option>2</option><option>3</option><option>4</option></select></div>
<button type="button" class="btn lb-btn btn-primary" data-bs-dismiss="modal">Add to basket</button>
</div>
<div class="d-flex flex-wrap align-items-center justify-content-between column-gap-3 row-gap-1 mt-3 lb-more"><a class="lb-link" href="#">View full details</a><span class="lb-note">Free delivery in 5–7 days</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
CSS
/* Bootstrap's Modal component as an image lightbox, a video player and a product quick view,
restyled through its --bs-modal-*, --bs-btn-* and --bs-backdrop-* variables. */
/* Triggers on the (light) page */
.lb-grid{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px}
.lb-thumb{grid-column:span 2;display:block;aspect-ratio:1;padding:0;border:0;border-radius:10px;background:#0b0b0f;overflow:hidden;cursor:zoom-in}
.lb-thumb:nth-child(-n+2){grid-column:span 3;aspect-ratio:3/2}
.lb-thumb img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .35s ease,opacity .2s}
.lb-thumb:hover img{transform:scale(1.05);opacity:.9}
.lb-thumb:focus-visible,.lb-poster:focus-visible{outline:3px solid #b45309;outline-offset:3px}
.lb-poster{position:relative;display:block;width:100%;aspect-ratio:16/9;padding:0;border:0;border-radius:14px;background:#0b0b0f;color:#fff;text-align:left;overflow:hidden}
.lb-poster img{display:block;width:100%;height:100%;object-fit:cover;transition:transform .35s ease}
.lb-poster:hover img{transform:scale(1.04)}
.lb-poster::after{content:"";position:absolute;inset:0;background:linear-gradient(to top,rgba(11,11,15,.9),rgba(11,11,15,.1) 60%)}
.lb-play{position:absolute;z-index:1;left:50%;top:44%;display:grid;place-items:center;width:58px;height:58px;border-radius:50%;background:rgba(255,255,255,.94);color:#0b0b0f;
transform:translate(-50%,-50%);box-shadow:0 10px 30px -8px rgba(0,0,0,.6);transition:transform .2s}
.lb-poster:hover .lb-play{transform:translate(-50%,-50%) scale(1.08)}
.lb-play svg{width:22px;height:22px;margin-left:3px}
.lb-poster-text{position:absolute;z-index:1;left:16px;right:74px;bottom:13px;line-height:1.3}
.lb-poster-text b{display:block;font-size:.95rem}
.lb-poster-text span{display:block;margin-top:2px;color:#e4e4e7;font-size:.78rem}
.lb-chip{position:absolute;z-index:1;right:14px;bottom:15px;padding:2px 8px;background:rgba(11,11,15,.8);font:600 .75rem/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
.lb-product{--bs-card-border-color:#e2e8f0;--bs-card-border-radius:14px;gap:14px;padding:12px;box-shadow:0 1px 2px rgba(15,23,42,.05)}
.lb-product img{flex:none;width:84px;height:84px;border-radius:10px;object-fit:cover}
.lb-product-name{margin:0;color:#0f172a;font-size:.95rem;font-weight:600;line-height:1.3}
.lb-product-price{margin:2px 0 10px;color:#475569;font-size:.875rem}
.lb-btn{--bs-btn-padding-x:16px;--bs-btn-padding-y:8px;--bs-btn-font-size:.875rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.2;--bs-btn-border-radius:10px;
--bs-btn-bg:#fff;--bs-btn-color:#0f172a;--bs-btn-border-color:#cbd5e1;--bs-btn-hover-bg:#f1f5f9;--bs-btn-hover-color:#0f172a;--bs-btn-hover-border-color:#94a3b8;
--bs-btn-active-bg:#e2e8f0;--bs-btn-active-color:#0f172a;--bs-btn-active-border-color:#94a3b8;
display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:40px}
.lb-btn.btn-primary{--bs-btn-bg:#f59e0b;--bs-btn-color:#0b0b0f;--bs-btn-border-color:#f59e0b;--bs-btn-hover-bg:#d97706;--bs-btn-hover-color:#0b0b0f;--bs-btn-hover-border-color:#d97706;
--bs-btn-active-bg:#b45309;--bs-btn-active-color:#fff;--bs-btn-active-border-color:#b45309;min-height:46px}
.lb-btn:focus-visible{outline:2px solid #b45309;outline-offset:2px;box-shadow:none}
/* The dialogs */
.lb{--bs-modal-margin:16px;--bs-modal-width:min(880px,100% - 32px);--bs-modal-bg:#0b0b0f;--bs-modal-color:#fafafa;--bs-modal-border-width:0;
--bs-modal-border-radius:16px;--bs-modal-inner-border-radius:16px;font-size:.95rem;line-height:1.5}
.lb .modal-dialog{--bs-modal-width:min(880px,100% - 32px)}
.lb .modal-content{overflow:hidden;box-shadow:0 0 0 1px rgba(255,255,255,.07),0 40px 90px -20px rgba(0,0,0,.85)}
.modal-backdrop{--bs-backdrop-bg:#050508;--bs-backdrop-opacity:.88}
/* Same entrance as the plain version: a short scale-in instead of Bootstrap's 50px drop */
.lb.fade .modal-dialog{transform:scale(.96);transition:transform .25s cubic-bezier(.2,.8,.2,1)}
.lb.show .modal-dialog{transform:none}
/* Caption row under the photo or video: title and meta, counter, close */
.lb-cap{display:flex;align-items:center;gap:14px;margin:0;padding:16px 18px 18px 22px}
.lb-cap-text{flex:1;min-width:0}
.lb-title{margin:0;font-size:1.05rem;font-weight:650;letter-spacing:-.01em;line-height:1.35}
.lb-meta{margin:2px 0 0;color:#a1a1aa;font-size:.85rem}
.lb-count{flex:none;padding:3px 10px;border:1px solid rgba(245,158,11,.45);color:#f59e0b;font-size:.8rem;font-weight:600;line-height:1.4;font-variant-numeric:tabular-nums}
.lb .btn-close{--bs-btn-close-bg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23e4e4e7' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M3.7 3.7l8.6 8.6M12.3 3.7l-8.6 8.6'/%3E%3C/svg%3E");
--bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-opacity:1;--bs-btn-close-focus-shadow:none;
box-sizing:border-box;flex:none;width:40px;height:40px;padding:0;border:1px solid rgba(255,255,255,.14);border-radius:50%;background-size:15px;transition:background-color .15s}
.lb .btn-close:hover{background-color:rgba(255,255,255,.1)}
.lb .btn-close:focus-visible,.lb-nav:focus-visible{outline:2px solid #f59e0b;outline-offset:2px}
/* 1. Lightbox: the photo box keeps a 3:2 ratio (no jump between photos) and shrinks to fit short screens */
.lb-photo .modal-dialog{--bs-modal-width:min(1040px,100% - 32px,(100dvh - 150px) * 1.5)}
.lb-media{position:relative;aspect-ratio:3/2;background:#000}
.lb-media img{display:block;width:100%;height:100%;object-fit:contain}
.lb-nav{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-bg:rgba(11,11,15,.62);--bs-btn-color:#fff;--bs-btn-border-color:rgba(255,255,255,.25);
--bs-btn-hover-bg:rgba(11,11,15,.88);--bs-btn-hover-color:#fff;--bs-btn-hover-border-color:rgba(255,255,255,.5);
--bs-btn-active-bg:rgba(11,11,15,.95);--bs-btn-active-color:#fff;--bs-btn-active-border-color:rgba(255,255,255,.5);
--bs-btn-focus-box-shadow:none;display:grid;place-items:center;width:46px;height:46px}
.lb-nav svg{width:22px;height:22px}
.lb-nav.is-prev{left:14px}
.lb-nav.is-next{right:14px}
/* 2. Video */
.lb-player{background:#000}
.lb-player video{background:#000}
.lb-player video:focus-visible{outline:2px solid #f59e0b;outline-offset:-2px}
/* 3. Product quick view: a white panel, two columns that stack on phones */
.lb-quick{--bs-modal-bg:#fff;--bs-modal-color:#0f172a}
.lb-quick .modal-dialog{--bs-modal-width:min(920px,100% - 32px)}
.lb-quick .modal-content{box-shadow:0 0 0 1px rgba(15,23,42,.06),0 40px 90px -20px rgba(0,0,0,.7)}
.lb-quick .btn-close{--bs-btn-close-bg:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23334155' stroke-width='1.6' stroke-linecap='round'%3E%3Cpath d='M3.7 3.7l8.6 8.6M12.3 3.7l-8.6 8.6'/%3E%3C/svg%3E");
position:absolute;z-index:2;top:12px;right:12px;border-color:#e2e8f0;background-color:#fff;box-shadow:0 4px 12px -4px rgba(15,23,42,.25)}
.lb-quick .btn-close:hover{background-color:#f1f5f9}
.lb-quick .btn-close:focus-visible{outline-color:#b45309}
.lb-quick .modal-body{padding:0;overscroll-behavior:contain}
.lb-quick .modal-body:focus-visible{outline:2px solid #b45309;outline-offset:-2px}
.lb-qv-media{background:#f4f1ec}
.lb-qv-media img{display:block;width:100%;height:100%;min-height:360px;object-fit:cover}
.lb-qv-body{padding:34px 30px 28px}
.lb-kicker{margin:0 0 6px;color:#b45309;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase}
.lb-qv-title{margin:0;padding-right:40px;font-size:1.45rem;font-weight:700;letter-spacing:-.015em;line-height:1.25}
.lb-price{margin:8px 0 0;font-size:1.3rem;font-weight:700}
.lb-rating{display:flex;align-items:center;gap:8px;margin:6px 0 0;color:#475569;font-size:.85rem}
.lb-stars{display:flex;gap:1px;color:#f59e0b}
.lb-stars svg{width:15px;height:15px}
.lb-desc{margin:14px 0 0;color:#475569;font-size:.9rem}
.lb-field{margin:18px 0 0;min-width:0}
.lb-label{display:block;float:none;width:auto;margin:0 0 8px;padding:0;color:#0f172a;font-size:.8rem;font-weight:600;line-height:1.5}
.lb-swatch{--bs-btn-padding-x:12px;--bs-btn-padding-y:6px;--bs-btn-font-size:.85rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.2;--bs-btn-border-radius:999px;
--bs-btn-color:#334155;--bs-btn-border-color:#cbd5e1;--bs-btn-hover-color:#334155;--bs-btn-hover-bg:transparent;--bs-btn-hover-border-color:#64748b;
--bs-btn-active-bg:#fff;--bs-btn-active-color:#0f172a;--bs-btn-active-border-color:#0f172a;--bs-btn-focus-box-shadow:none;
display:inline-flex;align-items:center;gap:7px;padding-left:6px}
.lb-swatches{gap:6px}
.lb-swatch::before{content:"";width:18px;height:18px;border-radius:50%;background:var(--swatch);box-shadow:inset 0 0 0 1px rgba(15,23,42,.18)}
.lb-swatches .btn-check:checked+.lb-swatch{box-shadow:inset 0 0 0 1px #0f172a;font-weight:600}
.lb-swatches .btn-check:focus-visible+.lb-swatch{outline:2px solid #b45309;outline-offset:2px}
.lb-select{--bs-border-color:#94a3b8;--bs-border-radius:10px;min-width:84px;min-height:46px;padding-left:14px;color:#0f172a;font-size:.9rem;cursor:pointer}
.lb-select:focus{border-color:#94a3b8;box-shadow:none}
.lb-select:focus-visible{outline:2px solid #b45309;outline-offset:2px}
.lb-buy{margin-top:20px}
.lb-buy .lb-btn{flex:1}
.lb-more{font-size:.85rem}
.lb-link{color:#0f172a;font-weight:600;text-underline-offset:3px}
.lb-link:hover{color:#b45309}
.lb-link:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
.lb-note{color:#475569}
@media (max-width:767.98px){
.lb-qv-media img{height:240px;min-height:0}
.lb-qv-body{padding:22px 20px 24px}
}
@media (max-width:600px){
.lb-cap{gap:10px;padding:12px 12px 14px 16px}
.lb-title{font-size:.95rem}
.lb-meta{font-size:.8rem}
.lb-nav{width:40px;height:40px}
.lb-nav.is-prev{left:8px}
.lb-nav.is-next{right:8px}
}
@media (prefers-reduced-motion:reduce){
.lb.fade .modal-dialog,.lb-thumb img,.lb-poster img,.lb-play,.lb-btn,.lb .btn-close,.lb-nav,.lb-swatch{transition:none}
.lb-thumb:hover img,.lb-poster:hover img{transform:none}
.lb-poster:hover .lb-play{transform:translate(-50%,-50%)}
}
JavaScript
// Lightbox: every thumbnail carries its photo in data-* attributes; one modal shows whichever is current.
(function () {
var box = document.getElementById('lb-lightbox');
if (!box) return;
var thumbs = Array.prototype.slice.call(document.querySelectorAll('[data-bs-target="#lb-lightbox"]'));
var img = box.querySelector('[data-lb-img]'), current = 0;
function show(i) {
current = (i + thumbs.length) % thumbs.length; // wrap around at both ends
var d = thumbs[current].dataset;
img.src = d.src; img.alt = d.alt;
box.querySelector('[data-lb-title]').textContent = d.title;
box.querySelector('[data-lb-place]').textContent = d.place;
box.querySelector('[data-lb-credit]').textContent = d.credit;
box.querySelector('[data-lb-count]').textContent = (current + 1) + ' / ' + thumbs.length;
new Image().src = thumbs[(current + 1) % thumbs.length].dataset.src; // warm the next photo
}
// Bootstrap passes the button that opened the modal as event.relatedTarget
box.addEventListener('show.bs.modal', function (e) {
var i = thumbs.indexOf(e.relatedTarget);
if (i > -1) show(i);
});
box.querySelector('[data-lb-prev]').addEventListener('click', function () { show(current - 1); });
box.querySelector('[data-lb-next]').addEventListener('click', function () { show(current + 1); });
box.addEventListener('keydown', function (e) { // Left and Right arrows step through the photos
if (e.key === 'ArrowLeft') { e.preventDefault(); show(current - 1); }
if (e.key === 'ArrowRight') { e.preventDefault(); show(current + 1); }
});
})();
// Video: stop playback when its modal starts to hide (Esc, close button or backdrop)
document.querySelectorAll('.modal').forEach(function (modal) {
var video = modal.querySelector('video');
if (video) modal.addEventListener('hide.bs.modal', function () { video.pause(); });
});
/* Entrance keyframes for the dialogs and their backdrop (used through motion-safe:open:animate-[…]) */
@keyframes lb-in { from { opacity: 0; transform: scale(.96); } }
@keyframes lb-fade { from { opacity: 0; } }
/* Stop the page scrolling behind an open modal */
html:has(dialog:modal) { overflow: hidden; }
JavaScript
// Open a dialog from any button with data-open="dialog-id". showModal() traps focus, closes on Esc
// and makes the page behind inert. Buttons inside <form method="dialog"> close it with no script.
document.querySelectorAll('[data-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true }); // focus back to the trigger
});
// A click on the backdrop lands on the <dialog> itself (its form fills the box), so close it
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); });
});
// Lightbox: every thumbnail carries its photo in data-* attributes; one dialog shows whichever is current.
(function () {
var box = document.getElementById('lb-lightbox');
if (!box) return;
var thumbs = Array.prototype.slice.call(document.querySelectorAll('[data-open="lb-lightbox"]'));
var img = box.querySelector('[data-lb-img]'), current = 0;
function show(i) {
current = (i + thumbs.length) % thumbs.length; // wrap around at both ends
var d = thumbs[current].dataset;
img.src = d.src; img.alt = d.alt;
box.querySelector('[data-lb-title]').textContent = d.title;
box.querySelector('[data-lb-place]').textContent = d.place;
box.querySelector('[data-lb-credit]').textContent = d.credit;
box.querySelector('[data-lb-count]').textContent = (current + 1) + ' / ' + thumbs.length;
new Image().src = thumbs[(current + 1) % thumbs.length].dataset.src; // warm the next photo
}
thumbs.forEach(function (t, i) { t.addEventListener('click', function () { show(i); }); });
box.querySelector('[data-lb-prev]').addEventListener('click', function () { show(current - 1); });
box.querySelector('[data-lb-next]').addEventListener('click', function () { show(current + 1); });
box.addEventListener('keydown', function (e) { // Left and Right arrows step through the photos
if (e.key === 'ArrowLeft') { e.preventDefault(); show(current - 1); }
if (e.key === 'ArrowRight') { e.preventDefault(); show(current + 1); }
});
})();
// Video: stop playback when its dialog closes (Esc, close button or backdrop)
document.querySelectorAll('dialog').forEach(function (dialog) {
var video = dialog.querySelector('video');
if (video) dialog.addEventListener('close', function () { video.pause(); });
});
About this set
Media Modals holds three dialogs built around photos, video and products. The image lightbox opens from a grid of five thumbnails into a single dialog that shows the large photo with a caption, a place and credit line, a 1 / 5 counter, previous and next buttons and a close button; the Left and Right arrow keys step through the photos and wrap at both ends. The video dialog plays a 16:9 HTML video with native controls, a captions track and preload none, so nothing downloads until play, and it pauses when the dialog closes. The product quick view is a white two-column panel with price, rating, colour swatches as a radio group, a quantity select and an Add to basket button, and it stacks on phones. Use the lightbox for galleries and portfolios, the video dialog for tours and trailers, and the quick view on shop listing pages. The plain CSS and Tailwind versions use the native dialog element with showModal(); the Bootstrap version uses the Modal plugin and fills the lightbox from event.relatedTarget. A short script reads each photo from data attributes on its thumbnail, and the photo box keeps a fixed 3:2 ratio, so the layout does not jump between images.
What’s included
One lightbox dialog for any number of photos, filled from data-* attributes on the thumbnails
Arrow-key and button navigation that wraps around, with a caption and counter that update
Fixed 3:2 photo box with object-fit: contain that shrinks to fit short screens
Video with native controls, a WebVTT captions track, preload none and pause on close
Product quick view with a labelled colour radio group and a quantity select
Accessibility
Each dialog is labelled by its visible heading, each thumbnail button names its photo (for example Open photo 2 of 5: Mist over the escarpment), and icon-only buttons have aria-labels. The native dialog and Bootstrap's Modal keep focus inside, close on Esc and return focus to the button that opened them; the lightbox caption is a polite live region, so the new title is announced after an arrow key. Every control shows a visible focus ring, the colour swatches are real radio buttons with visible names, and animations are switched off under prefers-reduced-motion.
Customise it
In the plain version change --lb-accent, --lb-bg and --lb-w on .lb and its .is-photo and .is-quick modifiers. In Bootstrap edit --bs-modal-bg, --bs-modal-width on .lb .modal-dialog, --bs-backdrop-opacity and the --bs-btn-* variables on .lb-btn and .lb-nav; in Tailwind swap bg-[#0b0b0f], text-[#f59e0b], outline-[#f59e0b] and the w-[min(…)] width on each dialog.