Open demo

CSS code

HTML
<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(); });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<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(); });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<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>
  <symbol id="lb-i-chev" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4 6 4 4 4-4"/></symbol>
</svg>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Image lightbox with captions</span>
<div class="grid grid-cols-6 gap-2">
  <button class="group block cursor-zoom-in overflow-hidden rounded-[10px] border-0 bg-[#0b0b0f] p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#b45309] col-span-3 aspect-[3/2]" 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 class="block size-full object-cover transition-[scale,opacity] duration-[350ms] ease-out group-hover:scale-105 group-hover:opacity-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" 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="group block cursor-zoom-in overflow-hidden rounded-[10px] border-0 bg-[#0b0b0f] p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#b45309] col-span-3 aspect-[3/2]" 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 class="block size-full object-cover transition-[scale,opacity] duration-[350ms] ease-out group-hover:scale-105 group-hover:opacity-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" 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="group block cursor-zoom-in overflow-hidden rounded-[10px] border-0 bg-[#0b0b0f] p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#b45309] col-span-2 aspect-square" 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 class="block size-full object-cover transition-[scale,opacity] duration-[350ms] ease-out group-hover:scale-105 group-hover:opacity-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" 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="group block cursor-zoom-in overflow-hidden rounded-[10px] border-0 bg-[#0b0b0f] p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#b45309] col-span-2 aspect-square" 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 class="block size-full object-cover transition-[scale,opacity] duration-[350ms] ease-out group-hover:scale-105 group-hover:opacity-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" 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="group block cursor-zoom-in overflow-hidden rounded-[10px] border-0 bg-[#0b0b0f] p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#b45309] col-span-2 aspect-square" 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 class="block size-full object-cover transition-[scale,opacity] duration-[350ms] ease-out group-hover:scale-105 group-hover:opacity-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" 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="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Video</span>
<button class="group relative block aspect-video w-full cursor-pointer overflow-hidden rounded-[14px] border-0 bg-[#0b0b0f] p-0 text-left text-white after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(11,11,15,.9),rgba(11,11,15,.1)_60%)] after:content-[''] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#b45309]" type="button" data-open="lb-video" aria-haspopup="dialog" aria-label="Watch the clip: Bloom, 5 seconds">
  <img class="block size-full object-cover transition-[scale] duration-[350ms] ease-out group-hover:scale-[1.04] motion-reduce:transition-none motion-reduce:group-hover:scale-100" 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="absolute top-[44%] left-1/2 z-1 grid size-[58px] -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-white/94 text-[#0b0b0f] shadow-[0_10px_30px_-8px_rgba(0,0,0,.6)] transition-[scale] duration-200 group-hover:scale-108 motion-reduce:transition-none motion-reduce:group-hover:scale-100" aria-hidden="true"><svg class="ml-[3px] size-[22px]"><use href="#lb-i-play"/></svg></span>
  <span class="absolute right-[74px] bottom-[13px] left-4 z-1 leading-[1.3]"><b class="block text-[.95rem]">Watch the clip</b><span class="mt-0.5 block text-[.78rem] text-[#e4e4e7]">Bloom · a CC0 time-lapse</span></span>
  <span class="absolute right-3.5 bottom-[15px] z-1 rounded-full bg-[rgba(11,11,15,.8)] px-2 py-0.5 font-mono text-[.75rem] leading-normal font-semibold text-white">0:05</span>
</button></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6474] uppercase">Product quick view</span>
<div class="flex items-center gap-3.5 rounded-[14px] border border-[#e2e8f0] bg-white p-3 shadow-[0_1px_2px_rgba(15,23,42,.05)]">
  <img class="size-[84px] flex-none rounded-[10px] object-cover" 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="m-0 text-[.95rem] leading-[1.3] font-semibold text-[#0f172a]">Alder lounge chair</p><p class="mt-0.5 mb-2.5 text-[.875rem] text-[#475569]">£349</p>
  <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold no-underline transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none border-[#cbd5e1] bg-white text-[#0f172a] hover:border-[#94a3b8] hover:bg-[#f1f5f9]" 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="m-auto max-h-[calc(100dvh-32px)] w-[min(1040px,calc(100%_-_32px),calc((100dvh_-_150px)_*_1.5))] max-w-none overflow-hidden overscroll-contain rounded-2xl border-0 p-0 text-[.95rem] leading-normal backdrop:bg-[rgba(5,5,8,.88)] open:flex open:flex-col motion-safe:open:animate-[lb-in_.25s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[lb-fade_.25s_ease-out] bg-[#0b0b0f] text-[#fafafa] shadow-[0_0_0_1px_rgba(255,255,255,.07),0_40px_90px_-20px_rgba(0,0,0,.85)]" id="lb-lightbox" aria-labelledby="lb-photo-title">
  <form class="relative m-0 flex min-h-0 flex-1 flex-col" method="dialog">
    <figure class="m-0">
      <div class="relative aspect-[3/2] bg-black">
        <img class="block size-full object-contain" 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="absolute top-1/2 grid size-[46px] -translate-y-1/2 cursor-pointer place-items-center rounded-full border border-white/25 bg-[rgba(11,11,15,.62)] p-0 text-white transition-colors duration-150 hover:border-white/50 hover:bg-[rgba(11,11,15,.88)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#f59e0b] motion-reduce:transition-none max-[600px]:size-10 left-3.5 max-[600px]:left-2" type="button" data-lb-prev aria-label="Previous photo"><svg class="size-[22px]" aria-hidden="true"><use href="#lb-i-prev"/></svg></button>
        <button class="absolute top-1/2 grid size-[46px] -translate-y-1/2 cursor-pointer place-items-center rounded-full border border-white/25 bg-[rgba(11,11,15,.62)] p-0 text-white transition-colors duration-150 hover:border-white/50 hover:bg-[rgba(11,11,15,.88)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#f59e0b] motion-reduce:transition-none max-[600px]:size-10 right-3.5 max-[600px]:right-2" type="button" data-lb-next aria-label="Next photo"><svg class="size-[22px]" aria-hidden="true"><use href="#lb-i-next"/></svg></button>
      </div>
      <figcaption class="m-0 flex items-center gap-3.5 pt-4 pr-[18px] pb-[18px] pl-[22px] max-[600px]:gap-2.5 max-[600px]:pt-3 max-[600px]:pr-3 max-[600px]:pb-3.5 max-[600px]:pl-4">
        <div class="min-w-0 flex-1" aria-live="polite">
          <h2 class="m-0 text-[1.05rem] leading-[1.35] font-[650] tracking-[-.01em] max-[600px]:text-[.95rem]" id="lb-photo-title" data-lb-title>Sea of cloud at last light</h2>
          <p class="mt-0.5 mb-0 text-[.85rem] text-[#a1a1aa] max-[600px]:text-[.8rem]"><span data-lb-place>High alpine ridge</span> · <span data-lb-credit>Photo: Unsplash</span></p>
        </div>
        <span class="flex-none rounded-full border border-[rgba(245,158,11,.45)] px-2.5 py-[3px] text-[.8rem] leading-[1.4] font-semibold whitespace-nowrap text-[#f59e0b] tabular-nums" data-lb-count>1 / 5</span>
        <button class="grid size-10 flex-none cursor-pointer place-items-center rounded-full border border-white/14 bg-transparent p-0 text-[#e4e4e7] transition-colors duration-150 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#f59e0b] motion-reduce:transition-none" value="close" aria-label="Close"><svg class="size-[18px]" 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="m-auto max-h-[calc(100dvh-32px)] w-[min(880px,calc(100%_-_32px))] max-w-none overflow-hidden overscroll-contain rounded-2xl border-0 p-0 text-[.95rem] leading-normal backdrop:bg-[rgba(5,5,8,.88)] open:flex open:flex-col motion-safe:open:animate-[lb-in_.25s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[lb-fade_.25s_ease-out] bg-[#0b0b0f] text-[#fafafa] shadow-[0_0_0_1px_rgba(255,255,255,.07),0_40px_90px_-20px_rgba(0,0,0,.85)]" id="lb-video" aria-labelledby="lb-video-title">
  <form class="relative m-0 flex min-h-0 flex-1 flex-col" method="dialog">
    <div class="aspect-video bg-black">
      <video class="block size-full bg-black focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#f59e0b]" 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="m-0 flex items-center gap-3.5 pt-4 pr-[18px] pb-[18px] pl-[22px] max-[600px]:gap-2.5 max-[600px]:pt-3 max-[600px]:pr-3 max-[600px]:pb-3.5 max-[600px]:pl-4">
      <div class="min-w-0 flex-1">
        <h2 class="m-0 text-[1.05rem] leading-[1.35] font-[650] tracking-[-.01em] max-[600px]:text-[.95rem]" id="lb-video-title">Bloom: a 5-second CC0 clip</h2>
        <p class="mt-0.5 mb-0 text-[.85rem] text-[#a1a1aa] max-[600px]:text-[.8rem]">Time-lapse of a red flower bud opening · CC0 sample video from MDN</p>
      </div>
      <button class="grid size-10 flex-none cursor-pointer place-items-center rounded-full border border-white/14 bg-transparent p-0 text-[#e4e4e7] transition-colors duration-150 hover:bg-white/10 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#f59e0b] motion-reduce:transition-none" value="close" aria-label="Close"><svg class="size-[18px]" aria-hidden="true"><use href="#lb-i-x"/></svg></button>
    </div>
  </form>
</dialog>

<!-- 3. Product quick view -->
<dialog class="m-auto max-h-[calc(100dvh-32px)] w-[min(920px,calc(100%_-_32px))] max-w-none overflow-hidden overscroll-contain rounded-2xl border-0 p-0 text-[.95rem] leading-normal backdrop:bg-[rgba(5,5,8,.88)] open:flex open:flex-col motion-safe:open:animate-[lb-in_.25s_cubic-bezier(.2,.8,.2,1)] motion-safe:open:backdrop:animate-[lb-fade_.25s_ease-out] bg-white text-[#0f172a] shadow-[0_0_0_1px_rgba(15,23,42,.06),0_40px_90px_-20px_rgba(0,0,0,.7)]" id="lb-quick" aria-labelledby="lb-quick-title">
  <form class="relative m-0 flex min-h-0 flex-1 flex-col" method="dialog">
    <button class="absolute top-3 right-3 z-1 grid size-10 cursor-pointer place-items-center rounded-full border border-[#e2e8f0] bg-white p-0 text-[#334155] shadow-[0_4px_12px_-4px_rgba(15,23,42,.25)] transition-colors duration-150 hover:bg-[#f1f5f9] hover:text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none" value="close" aria-label="Close"><svg class="size-[18px]" aria-hidden="true"><use href="#lb-i-x"/></svg></button>
    <div class="grid min-h-0 grid-cols-1 overflow-y-auto overscroll-contain focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[#b45309] md:grid-cols-2" tabindex="0" role="region" aria-label="Alder lounge chair details">
      <div class="bg-[#f4f1ec]"><img class="block h-60 w-full object-cover md:h-full md:min-h-[360px]" 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="px-5 pt-[22px] pb-6 md:px-[30px] md:pt-[34px] md:pb-7">
        <p class="mt-0 mb-1.5 text-[.72rem] font-bold tracking-[.1em] text-[#b45309] uppercase">Alder collection</p>
        <h2 class="m-0 pr-10 text-[1.45rem] leading-[1.25] font-bold tracking-[-.015em]" id="lb-quick-title">Alder lounge chair</h2>
        <p class="mt-2 mb-0 text-[1.3rem] font-bold">£349</p>
        <p class="mt-1.5 mb-0 flex items-center gap-2 text-[.85rem] text-[#475569]"><span class="flex gap-px text-[#f59e0b]" aria-hidden="true"><svg class="size-[15px]"><use href="#lb-i-star"/></svg><svg class="size-[15px]"><use href="#lb-i-star"/></svg><svg class="size-[15px]"><use href="#lb-i-star"/></svg><svg class="size-[15px]"><use href="#lb-i-star"/></svg><svg class="size-[15px]"><use href="#lb-i-star"/></svg></span>4.7 out of 5 · 212 reviews</p>
        <p class="mt-3.5 mb-0 text-[.9rem] text-[#475569]">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="mt-[18px] mb-0 min-w-0 border-0 p-0">
          <legend class="mb-2 block p-0 text-[.8rem] font-semibold text-[#0f172a]">Colour</legend>
          <div class="flex flex-wrap gap-1.5">
            <label class="relative cursor-pointer"><input class="peer sr-only" type="radio" name="lb-colour" value="mustard" checked><span class="inline-flex items-center gap-[7px] rounded-full border border-[#cbd5e1] py-1.5 pr-3 pl-1.5 text-[.85rem] leading-[1.2] text-[#334155] transition-[border-color,box-shadow] duration-150 before:size-[18px] before:rounded-full before:bg-(--swatch) before:shadow-[inset_0_0_0_1px_rgba(15,23,42,.18)] before:content-[] hover:border-[#64748b] peer-checked:border-[#0f172a] peer-checked:font-semibold peer-checked:text-[#0f172a] peer-checked:shadow-[inset_0_0_0_1px_#0f172a] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#b45309] motion-reduce:transition-none" style="--swatch:#d9a21b">Mustard</span></label>
            <label class="relative cursor-pointer"><input class="peer sr-only" type="radio" name="lb-colour" value="sage"><span class="inline-flex items-center gap-[7px] rounded-full border border-[#cbd5e1] py-1.5 pr-3 pl-1.5 text-[.85rem] leading-[1.2] text-[#334155] transition-[border-color,box-shadow] duration-150 before:size-[18px] before:rounded-full before:bg-(--swatch) before:shadow-[inset_0_0_0_1px_rgba(15,23,42,.18)] before:content-[] hover:border-[#64748b] peer-checked:border-[#0f172a] peer-checked:font-semibold peer-checked:text-[#0f172a] peer-checked:shadow-[inset_0_0_0_1px_#0f172a] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#b45309] motion-reduce:transition-none" style="--swatch:#9caf88">Sage</span></label>
            <label class="relative cursor-pointer"><input class="peer sr-only" type="radio" name="lb-colour" value="charcoal"><span class="inline-flex items-center gap-[7px] rounded-full border border-[#cbd5e1] py-1.5 pr-3 pl-1.5 text-[.85rem] leading-[1.2] text-[#334155] transition-[border-color,box-shadow] duration-150 before:size-[18px] before:rounded-full before:bg-(--swatch) before:shadow-[inset_0_0_0_1px_rgba(15,23,42,.18)] before:content-[] hover:border-[#64748b] peer-checked:border-[#0f172a] peer-checked:font-semibold peer-checked:text-[#0f172a] peer-checked:shadow-[inset_0_0_0_1px_#0f172a] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#b45309] motion-reduce:transition-none" style="--swatch:#3f3f46">Charcoal</span></label>
            <label class="relative cursor-pointer"><input class="peer sr-only" type="radio" name="lb-colour" value="oat"><span class="inline-flex items-center gap-[7px] rounded-full border border-[#cbd5e1] py-1.5 pr-3 pl-1.5 text-[.85rem] leading-[1.2] text-[#334155] transition-[border-color,box-shadow] duration-150 before:size-[18px] before:rounded-full before:bg-(--swatch) before:shadow-[inset_0_0_0_1px_rgba(15,23,42,.18)] before:content-[] hover:border-[#64748b] peer-checked:border-[#0f172a] peer-checked:font-semibold peer-checked:text-[#0f172a] peer-checked:shadow-[inset_0_0_0_1px_#0f172a] peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[#b45309] motion-reduce:transition-none" style="--swatch:#e7dcc8">Oat</span></label>
          </div>
        </fieldset>
        <div class="mt-5 flex items-end gap-2.5">
          <div><label class="mb-2 block p-0 text-[.8rem] font-semibold text-[#0f172a]" for="lb-qty">Quantity</label>
          <div class="relative"><select class="min-h-[46px] min-w-[84px] cursor-pointer appearance-none rounded-[10px] border border-[#94a3b8] bg-white pr-[34px] pl-3.5 text-[.9rem] text-[#0f172a] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#b45309]" id="lb-qty" name="qty"><option>1</option><option>2</option><option>3</option><option>4</option></select><svg class="pointer-events-none absolute top-1/2 right-3 size-3.5 -translate-y-1/2 text-[#334155]" aria-hidden="true"><use href="#lb-i-chev"/></svg></div></div>
          <button class="inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-[10px] border px-4 py-2 text-[.875rem] leading-[1.2] font-semibold no-underline transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none min-h-[46px] flex-1 border-[#f59e0b] bg-[#f59e0b] text-[#0b0b0f] hover:border-[#d97706] hover:bg-[#d97706]" value="add">Add to basket</button>
        </div>
        <div class="mt-4 flex flex-wrap items-center justify-between gap-x-4 gap-y-1.5 text-[.85rem]"><a class="font-semibold text-[#0f172a] underline underline-offset-3 hover:text-[#b45309] focus-visible:rounded-xs focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[#b45309]" href="#">View full details</a><span class="text-[#475569]">Free delivery in 5–7 days</span></div>
      </div>
    </div>
  </form>
</dialog>
CSS
/* 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.