Open demo

CSS code

HTML
<div><span class="lbl">Photo grid that opens a lightbox</span>
<section class="lbg" data-lightbox aria-labelledby="wed-title">
  <div class="gh"><div><p class="gh-kicker">Juniper & Gold</p><h2 id="wed-title">Maren & Theo</h2></div><p class="gh-meta">6 September 2025 · 6 of 214 photographs</p></div>
  <ul class="lbg-grid" role="list">
    <li><a class="lb-thumb" href="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Just married, 18:42" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=640&h=480&q=70" width="640" height="480" loading="lazy" alt="A bride holding a bouquet of pale roses beside the groom, lit from behind by low sun"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="lb-thumb" href="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Cheers by the pool" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=640&h=480&q=70" width="640" height="480" loading="lazy" alt="A bride and groom raising their joined hands by a pool, with guests and red and white balloons behind them"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="lb-thumb" href="https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Hands" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=640&h=480&q=70" width="640" height="480" loading="lazy" alt="Close-up of the couple’s hands with wedding rings, resting on a bouquet of peach roses"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="lb-thumb" href="https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Two chairs on the lawn" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=640&h=480&q=70" width="640" height="480" loading="lazy" alt="Two white chairs decorated with flowers on a lawn by the water"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="lb-thumb" href="https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="The ceremony aisle" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=640&h=480&q=70" width="640" height="480" loading="lazy" alt="Rows of white chairs along a pink aisle leading to a flower-covered gazebo"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="lb-thumb" href="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="The rings" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=640&h=480&q=70" width="640" height="480" loading="lazy" alt="Two gold wedding rings resting on pink peonies"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
  </ul>
  <dialog class="lb-dialog" data-lb-dialog aria-labelledby="wed-cap">
    <button class="lb-btn lb-close" type="button" data-lb-close aria-label="Close photo viewer" autofocus><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
    <figure class="lb-stage"><img class="lb-img" data-lb-img alt=""><figcaption class="lb-bar"><span id="wed-cap" data-lb-cap></span><span class="lb-count" data-lb-count></span></figcaption></figure>
    <button class="lb-btn lb-prev" type="button" data-lb-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
    <button class="lb-btn lb-next" type="button" data-lb-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
  </dialog>
</section></div>

<div><span class="lbl">Article images that open full screen</span>
<article class="fd" data-lightbox aria-labelledby="fd-title">
  <p class="gh-kicker">Field diary · Winter</p>
  <h2 id="fd-title">Edge of the Woods</h2>
  <p class="fd-text">Two visitors in one month at the hide above the beech wood. The fox came at first light and stayed for eleven minutes; the stag waited until a Friday evening, when the light went amber.</p>
  <div class="fd-figs">
    <figure><a class="lb-thumb" href="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Tuesday, 07:12. The fox on the snow bank." aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=720&h=540&q=70" width="720" height="540" loading="lazy" alt="A red fox standing on a snowy bank, looking to one side"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a><figcaption>Tuesday, 07:12. The fox on the snow bank.</figcaption></figure>
    <figure><a class="lb-thumb" href="https://images.unsplash.com/photo-1484406566174-9da000fda645?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Friday, 18:05. The stag at the wood’s edge." aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1484406566174-9da000fda645?auto=format&fit=crop&w=720&h=540&q=70" width="720" height="540" loading="lazy" alt="A stag with wide antlers standing at the edge of woodland in evening light"><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a><figcaption>Friday, 18:05. The stag at the wood’s edge.</figcaption></figure>
  </div>
  <dialog class="lb-dialog" data-lb-dialog aria-labelledby="fd-cap">
    <button class="lb-btn lb-close" type="button" data-lb-close aria-label="Close photo viewer" autofocus><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
    <figure class="lb-stage"><img class="lb-img" data-lb-img alt=""><figcaption class="lb-bar"><span id="fd-cap" data-lb-cap></span><span class="lb-count" data-lb-count></span></figcaption></figure>
    <button class="lb-btn lb-prev" type="button" data-lb-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
    <button class="lb-btn lb-next" type="button" data-lb-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
  </dialog>
</article></div>
CSS
/* Lightbox galleries: thumbnails are links to the full photo, so they still work without the script.
   The script opens a native <dialog> with showModal(): focus moves into it and stays there,
   Esc closes it, the arrow keys step through the photos and focus returns to the thumbnail. */
.gh{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:6px 24px;margin-bottom:20px}
.gh-kicker{font-size:.75rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#8c4a3c}
.gh h2{margin-top:4px;font-size:1.75rem;line-height:1.15;letter-spacing:-.02em}
.gh-meta{font-size:.9rem;color:#6b5d58}

/* Thumbnails */
.lbg-grid{list-style:none;display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
@media (min-width:768px){.lbg-grid{grid-template-columns:repeat(3,1fr)}}
.lb-thumb{position:relative;display:block;border-radius:12px;overflow:hidden;background:#e4dbd6;cursor:zoom-in}
.lb-thumb img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;display:block;transition:transform .5s ease,filter .3s}
.lb-thumb:hover img{transform:scale(1.04);filter:brightness(.9)}
.lb-zoom{position:absolute;top:10px;right:10px;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:rgba(255,255,255,.92);color:#2a2220}
.lb-zoom svg{width:16px;height:16px}
.lb-thumb:focus-visible{outline:3px solid #8c4a3c;outline-offset:3px}

/* Article layout */
.fd{max-width:760px}
.fd h2{margin-top:4px;font-size:1.75rem;line-height:1.15;letter-spacing:-.02em}
.fd-text{margin:12px 0 22px;font-size:1.02rem;line-height:1.65;color:#3d3330}
.fd-figs{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}
.fd-figs figcaption{margin-top:8px;font-size:.82rem;line-height:1.45;color:#6b5d58}
@media (max-width:560px){.fd-figs{grid-template-columns:1fr}}

/* The lightbox dialog */
.lb-dialog{position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;padding:64px 80px 28px;border:0;background:transparent;color:#fff}
.lb-dialog::backdrop{background:rgba(14,11,10,.96)}
.lb-dialog[open]{display:flex;justify-content:center;animation:lb-in .25s ease-out}
@keyframes lb-in{from{opacity:0;transform:scale(.97)}}
.lb-stage{display:flex;flex-direction:column;gap:14px;width:100%;max-width:1200px;height:100%;margin:0}
.lb-img{flex:1;min-height:0;width:100%;object-fit:contain}
.lb-bar{display:flex;justify-content:space-between;gap:16px;font-size:.92rem;color:#f2ece8}
.lb-count{flex:none;color:#cfc6c1;font-variant-numeric:tabular-nums}
.lb-btn{position:absolute;display:grid;place-items:center;width:46px;height:46px;padding:0;border:0;border-radius:50%;background:rgba(255,255,255,.14);color:#fff;cursor:pointer;transition:background-color .2s}
.lb-btn svg{width:20px;height:20px}
.lb-btn:hover{background:rgba(255,255,255,.28)}
.lb-btn:focus-visible{outline:3px solid #f3b9a6;outline-offset:3px}
.lb-close{top:12px;right:16px}
.lb-prev,.lb-next{top:50%;margin-top:-23px}
.lb-prev{left:16px}.lb-next{right:16px}
@media (max-width:640px){.lb-dialog{padding:68px 12px 88px}.lb-prev,.lb-next{top:auto;bottom:20px;margin:0}.lb-prev{left:calc(50% - 58px)}.lb-next{right:calc(50% - 58px)}}
@media (prefers-reduced-motion:reduce){.lb-thumb img,.lb-btn{transition:none}.lb-thumb:hover img{transform:none}.lb-dialog[open]{animation:none}}
JavaScript
// Lightbox: one <dialog> per [data-lightbox] group. Links keep their href, so without
// this script a click simply opens the full-size photo.
document.querySelectorAll('[data-lightbox]').forEach((root) => {
  const items = [...root.querySelectorAll('[data-lb-item]')];
  const dlg = root.querySelector('[data-lb-dialog]');
  const img = dlg.querySelector('[data-lb-img]');
  const cap = dlg.querySelector('[data-lb-cap]');
  const count = dlg.querySelector('[data-lb-count]');
  const arrows = dlg.querySelectorAll('[data-lb-prev], [data-lb-next]');
  let current = 0;
  const show = (n) => {
    current = (n + items.length) % items.length;
    const link = items[current];
    img.src = link.href;
    img.alt = link.querySelector('img').alt;
    cap.textContent = link.dataset.caption || '';
    count.textContent = items.length > 1 ? (current + 1) + ' of ' + items.length : '';
  };
  arrows.forEach((b) => { b.hidden = items.length < 2; });
  items.forEach((link, n) => link.addEventListener('click', (e) => {
    e.preventDefault();
    show(n);
    dlg.showModal();
  }));
  dlg.querySelector('[data-lb-prev]').addEventListener('click', () => show(current - 1));
  dlg.querySelector('[data-lb-next]').addEventListener('click', () => show(current + 1));
  dlg.querySelector('[data-lb-close]').addEventListener('click', () => dlg.close());
  dlg.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft') show(current - 1);
    if (e.key === 'ArrowRight') show(current + 1);
  });
  dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });  // click on the dark area
  dlg.addEventListener('close', () => items[current].focus());                   // back to the photo you were on
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div><span class="lbl">Photo grid that opens a lightbox</span>
<section class="lbg" data-lightbox aria-labelledby="wed-title">
  <div class="gh d-flex flex-wrap align-items-end justify-content-between"><div><p class="gh-kicker">Juniper & Gold</p><h2 id="wed-title" class="gh-title">Maren & Theo</h2></div><p class="gh-meta">6 September 2025 · 6 of 214 photographs</p></div>
  <div class="row row-cols-2 row-cols-md-3 g-3">
    <div class="col"><a class="lb-thumb" href="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#wed-lb" data-lb-item data-caption="Just married, 18:42" aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=640&h=480&q=70" class="object-fit-cover" width="640" height="480" loading="lazy" alt="A bride holding a bouquet of pale roses beside the groom, lit from behind by low sun"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></div>
    <div class="col"><a class="lb-thumb" href="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#wed-lb" data-lb-item data-caption="Cheers by the pool" aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=640&h=480&q=70" class="object-fit-cover" width="640" height="480" loading="lazy" alt="A bride and groom raising their joined hands by a pool, with guests and red and white balloons behind them"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></div>
    <div class="col"><a class="lb-thumb" href="https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#wed-lb" data-lb-item data-caption="Hands" aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=640&h=480&q=70" class="object-fit-cover" width="640" height="480" loading="lazy" alt="Close-up of the couple’s hands with wedding rings, resting on a bouquet of peach roses"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></div>
    <div class="col"><a class="lb-thumb" href="https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#wed-lb" data-lb-item data-caption="Two chairs on the lawn" aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=640&h=480&q=70" class="object-fit-cover" width="640" height="480" loading="lazy" alt="Two white chairs decorated with flowers on a lawn by the water"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></div>
    <div class="col"><a class="lb-thumb" href="https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#wed-lb" data-lb-item data-caption="The ceremony aisle" aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=640&h=480&q=70" class="object-fit-cover" width="640" height="480" loading="lazy" alt="Rows of white chairs along a pink aisle leading to a flower-covered gazebo"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></div>
    <div class="col"><a class="lb-thumb" href="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#wed-lb" data-lb-item data-caption="The rings" aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=640&h=480&q=70" class="object-fit-cover" width="640" height="480" loading="lazy" alt="Two gold wedding rings resting on pink peonies"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></div>
  </div>
  <div class="modal fade lb-modal" id="wed-lb" tabindex="-1" aria-labelledby="wed-cap" aria-hidden="true" data-bs-theme="dark">
    <div class="modal-dialog modal-fullscreen"><div class="modal-content">
      <button type="button" class="btn-close lb-btn lb-close" data-bs-dismiss="modal" aria-label="Close photo viewer"></button>
      <figure class="modal-body lb-stage" data-lb-body><img class="lb-img" data-lb-img alt=""><figcaption class="lb-bar"><span id="wed-cap" data-lb-cap></span><span class="lb-count" data-lb-count></span></figcaption></figure>
      <button class="lb-btn lb-prev" type="button" data-lb-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
      <button class="lb-btn lb-next" type="button" data-lb-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
    </div></div>
  </div>
</section></div>

<div><span class="lbl">Article images that open full screen</span>
<article class="fd" data-lightbox aria-labelledby="fd-title">
  <p class="gh-kicker">Field diary · Winter</p>
  <h2 id="fd-title" class="gh-title">Edge of the Woods</h2>
  <p class="fd-text">Two visitors in one month at the hide above the beech wood. The fox came at first light and stayed for eleven minutes; the stag waited until a Friday evening, when the light went amber.</p>
  <div class="row row-cols-1 row-cols-sm-2 g-3">
    <div class="col"><figure class="figure m-0 d-block"><a class="lb-thumb" href="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#fd-lb" data-lb-item data-caption="Tuesday, 07:12. The fox on the snow bank." aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=720&h=540&q=70" class="object-fit-cover" width="720" height="540" loading="lazy" alt="A red fox standing on a snowy bank, looking to one side"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a><figcaption class="figure-caption">Tuesday, 07:12. The fox on the snow bank.</figcaption></figure></div>
    <div class="col"><figure class="figure m-0 d-block"><a class="lb-thumb" href="https://images.unsplash.com/photo-1484406566174-9da000fda645?auto=format&fit=crop&w=1800&q=70" data-bs-toggle="modal" data-bs-target="#fd-lb" data-lb-item data-caption="Friday, 18:05. The stag at the wood’s edge." aria-haspopup="dialog"><span class="ratio ratio-4x3 d-block"><img src="https://images.unsplash.com/photo-1484406566174-9da000fda645?auto=format&fit=crop&w=720&h=540&q=70" class="object-fit-cover" width="720" height="540" loading="lazy" alt="A stag with wide antlers standing at the edge of woodland in evening light"></span><span class="lb-zoom"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a><figcaption class="figure-caption">Friday, 18:05. The stag at the wood’s edge.</figcaption></figure></div>
  </div>
  <div class="modal fade lb-modal" id="fd-lb" tabindex="-1" aria-labelledby="fd-cap" aria-hidden="true" data-bs-theme="dark">
    <div class="modal-dialog modal-fullscreen"><div class="modal-content">
      <button type="button" class="btn-close lb-btn lb-close" data-bs-dismiss="modal" aria-label="Close photo viewer"></button>
      <figure class="modal-body lb-stage" data-lb-body><img class="lb-img" data-lb-img alt=""><figcaption class="lb-bar"><span id="fd-cap" data-lb-cap></span><span class="lb-count" data-lb-count></span></figcaption></figure>
      <button class="lb-btn lb-prev" type="button" data-lb-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
      <button class="lb-btn lb-next" type="button" data-lb-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
    </div></div>
  </div>
</article></div>
CSS
/* Lightbox galleries on Bootstrap's .modal (.modal-fullscreen, dark theme). Thumbnails keep their
   href, so without the bundle a click opens the full photo. Bootstrap traps focus, closes on Esc
   and the small script adds the arrows, the arrow keys and the caption. */
.gh{gap:6px 24px;margin-bottom:20px}
.gh-kicker{margin:0;font-size:.75rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#8c4a3c}
.gh-title{margin:4px 0 0;font-size:1.75rem;font-weight:700;line-height:1.15;letter-spacing:-.02em}
.gh-meta{margin:0;font-size:.9rem;color:#6b5d58}

/* Thumbnails */
.lb-thumb{position:relative;display:block;border-radius:12px;overflow:hidden;background:#e4dbd6;cursor:zoom-in}
.lb-thumb img{transition:transform .5s ease,filter .3s}
.lb-thumb:hover img{transform:scale(1.04);filter:brightness(.9)}
.lb-zoom{position:absolute;top:10px;right:10px;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:rgba(255,255,255,.92);color:#2a2220}
.lb-zoom svg{width:16px;height:16px}
.lb-thumb:focus-visible{outline:3px solid #8c4a3c;outline-offset:3px}

/* Article layout */
.fd{max-width:760px}
.fd-text{margin:12px 0 22px;font-size:1.02rem;line-height:1.65;color:#3d3330}
.fd .figure-caption{margin-top:8px;font-size:.82rem;line-height:1.45;color:#6b5d58}

/* The lightbox modal */
.lb-modal .modal-content{padding:64px 80px 28px;background:rgba(14,11,10,.92);color:#fff;align-items:center}
.lb-stage{display:flex;flex-direction:column;gap:14px;width:100%;max-width:1200px;height:100%;margin:0;padding:0;overflow:hidden}
.lb-img{flex:1;min-height:0;width:100%;object-fit:contain}
.lb-bar{display:flex;justify-content:space-between;gap:16px;font-size:.92rem;color:#f2ece8}
.lb-count{flex:none;color:#cfc6c1;font-variant-numeric:tabular-nums}
.lb-btn{position:absolute;display:grid;place-items:center;width:46px;height:46px;padding:0;border:0;border-radius:50%;background-color:rgba(255,255,255,.14);color:#fff;opacity:1;cursor:pointer;transition:background-color .2s}
.lb-btn svg{width:20px;height:20px}
.lb-btn.btn-close{--bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-opacity:1;filter:none;background-size:15px;
  --bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e")}
.lb-btn:hover{background-color:rgba(255,255,255,.28);color:#fff}
.lb-btn:focus-visible,.lb-btn.btn-close:focus{box-shadow:none;outline:3px solid #f3b9a6;outline-offset:3px}
.lb-btn.btn-close:focus:not(:focus-visible){outline:0}
.lb-close{top:12px;right:16px}
.lb-prev,.lb-next{top:50%;margin-top:-23px}
.lb-prev{left:16px}.lb-next{right:16px}
@media (max-width:640px){.lb-modal .modal-content{padding:68px 12px 88px}.lb-prev,.lb-next{top:auto;bottom:20px;margin:0}.lb-prev{left:calc(50% - 58px)}.lb-next{right:calc(50% - 58px)}}
@media (prefers-reduced-motion:reduce){.lb-thumb img,.lb-btn{transition:none}.lb-thumb:hover img{transform:none}}
JavaScript
// Lightbox on Bootstrap's modal: fills in the photo from the thumbnail that opened it,
// adds previous/next buttons and arrow keys, and returns focus to the matching thumbnail.
document.querySelectorAll('[data-lightbox]').forEach((root) => {
  const items = [...root.querySelectorAll('[data-lb-item]')];
  const el = root.querySelector('.modal');
  const modal = bootstrap.Modal.getOrCreateInstance(el);
  const img = el.querySelector('[data-lb-img]');
  const cap = el.querySelector('[data-lb-cap]');
  const count = el.querySelector('[data-lb-count]');
  let current = 0;
  const show = (n) => {
    current = (n + items.length) % items.length;
    const link = items[current];
    img.src = link.href;
    img.alt = link.querySelector('img').alt;
    cap.textContent = link.dataset.caption || '';
    count.textContent = items.length > 1 ? (current + 1) + ' of ' + items.length : '';
  };
  el.querySelectorAll('[data-lb-prev], [data-lb-next]').forEach((b) => { b.hidden = items.length < 2; });
  el.addEventListener('show.bs.modal', (e) => show(Math.max(0, items.indexOf(e.relatedTarget))));
  el.querySelector('[data-lb-prev]').addEventListener('click', () => show(current - 1));
  el.querySelector('[data-lb-next]').addEventListener('click', () => show(current + 1));
  el.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft') show(current - 1);
    if (e.key === 'ArrowRight') show(current + 1);
  });
  el.querySelector('[data-lb-body]').addEventListener('click', (e) => { if (e.target === e.currentTarget) modal.hide(); });
  el.addEventListener('hidden.bs.modal', () => setTimeout(() => items[current].focus()));
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b5d58] uppercase">Photo grid that opens a lightbox</span>
<section data-lightbox aria-labelledby="wed-title">
  <div class="mb-5 flex flex-wrap items-end justify-between gap-x-6 gap-y-1.5"><div><p class="text-xs leading-[normal] font-bold tracking-[.14em] text-[#8c4a3c] uppercase">Juniper & Gold</p><h2 id="wed-title" class="mt-1 text-[1.75rem] leading-[1.15] font-bold tracking-[-.02em]">Maren & Theo</h2></div><p class="text-[.9rem] leading-[normal] text-[#6b5d58]">6 September 2025 · 6 of 214 photographs</p></div>
  <ul class="grid grid-cols-2 gap-3.5 md:grid-cols-3" role="list">
    <li><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Just married, 18:42" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1519741497674-611481863552?auto=format&fit=crop&w=640&h=480&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="640" height="480" loading="lazy" alt="A bride holding a bouquet of pale roses beside the groom, lit from behind by low sun"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Cheers by the pool" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=640&h=480&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="640" height="480" loading="lazy" alt="A bride and groom raising their joined hands by a pool, with guests and red and white balloons behind them"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Hands" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1465495976277-4387d4b0b4c6?auto=format&fit=crop&w=640&h=480&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="640" height="480" loading="lazy" alt="Close-up of the couple’s hands with wedding rings, resting on a bouquet of peach roses"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Two chairs on the lawn" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1522673607200-164d1b6ce486?auto=format&fit=crop&w=640&h=480&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="640" height="480" loading="lazy" alt="Two white chairs decorated with flowers on a lawn by the water"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="The ceremony aisle" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1523438885200-e635ba2c371e?auto=format&fit=crop&w=640&h=480&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="640" height="480" loading="lazy" alt="Rows of white chairs along a pink aisle leading to a flower-covered gazebo"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
    <li><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="The rings" aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1515934751635-c81c6bc9a2d8?auto=format&fit=crop&w=640&h=480&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="640" height="480" loading="lazy" alt="Two gold wedding rings resting on pink peonies"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a></li>
  </ul>
  <dialog class="fixed inset-0 m-0 size-full max-h-none max-w-none border-0 bg-transparent px-3 pt-[68px] pb-[88px] text-white backdrop:bg-[rgba(14,11,10,.96)] open:flex open:animate-lb-in open:justify-center sm:px-20 sm:pt-16 sm:pb-7 motion-reduce:open:animate-none" data-lb-dialog aria-labelledby="wed-cap">
    <button class="absolute grid size-[46px] cursor-pointer place-items-center rounded-full bg-white/14 text-white transition-colors duration-200 hover:bg-white/28 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#f3b9a6] motion-reduce:transition-none [&>svg]:size-5 top-3 right-4" type="button" data-lb-close aria-label="Close photo viewer" autofocus><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
    <figure class="flex size-full max-w-[1200px] flex-col gap-3.5"><img class="min-h-0 w-full flex-1 object-contain" data-lb-img alt=""><figcaption class="flex justify-between gap-4 text-[.92rem] text-[#f2ece8]"><span id="wed-cap" data-lb-cap></span><span class="flex-none text-[#cfc6c1] tabular-nums" data-lb-count></span></figcaption></figure>
    <button class="absolute grid size-[46px] cursor-pointer place-items-center rounded-full bg-white/14 text-white transition-colors duration-200 hover:bg-white/28 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#f3b9a6] motion-reduce:transition-none [&>svg]:size-5 bottom-5 left-[calc(50%-58px)] sm:top-1/2 sm:bottom-auto sm:left-4 sm:-mt-[23px]" type="button" data-lb-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
    <button class="absolute grid size-[46px] cursor-pointer place-items-center rounded-full bg-white/14 text-white transition-colors duration-200 hover:bg-white/28 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#f3b9a6] motion-reduce:transition-none [&>svg]:size-5 right-[calc(50%-58px)] bottom-5 sm:top-1/2 sm:right-4 sm:bottom-auto sm:-mt-[23px]" type="button" data-lb-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
  </dialog>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b5d58] uppercase">Article images that open full screen</span>
<article class="max-w-[760px]" data-lightbox aria-labelledby="fd-title">
  <p class="text-xs leading-[normal] font-bold tracking-[.14em] text-[#8c4a3c] uppercase">Field diary · Winter</p>
  <h2 id="fd-title" class="mt-1 text-[1.75rem] leading-[1.15] font-bold tracking-[-.02em]">Edge of the Woods</h2>
  <p class="mt-3 mb-[22px] text-[1.02rem] leading-[1.65] text-[#3d3330]">Two visitors in one month at the hide above the beech wood. The fox came at first light and stayed for eleven minutes; the stag waited until a Friday evening, when the light went amber.</p>
  <div class="grid gap-4 min-[561px]:grid-cols-2">
    <figure><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Tuesday, 07:12. The fox on the snow bank." aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1474511320723-9a56873867b5?auto=format&fit=crop&w=720&h=540&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="720" height="540" loading="lazy" alt="A red fox standing on a snowy bank, looking to one side"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a><figcaption class="mt-2 text-[.82rem] leading-[1.45] text-[#6b5d58]">Tuesday, 07:12. The fox on the snow bank.</figcaption></figure>
    <figure><a class="group relative block cursor-zoom-in overflow-hidden rounded-xl bg-[#e4dbd6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#8c4a3c]" href="https://images.unsplash.com/photo-1484406566174-9da000fda645?auto=format&fit=crop&w=1800&q=70" data-lb-item data-caption="Friday, 18:05. The stag at the wood’s edge." aria-haspopup="dialog"><img src="https://images.unsplash.com/photo-1484406566174-9da000fda645?auto=format&fit=crop&w=720&h=540&q=70" class="block aspect-[4/3] h-auto w-full object-cover transition-[scale,filter] duration-500 group-hover:scale-104 group-hover:brightness-90 motion-reduce:transition-none motion-reduce:group-hover:scale-100" width="720" height="540" loading="lazy" alt="A stag with wide antlers standing at the edge of woodland in evening light"><span class="absolute top-2.5 right-2.5 grid size-[34px] place-items-center rounded-full bg-white/92 text-[#2a2220] [&>svg]:size-4"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg></span></a><figcaption class="mt-2 text-[.82rem] leading-[1.45] text-[#6b5d58]">Friday, 18:05. The stag at the wood’s edge.</figcaption></figure>
  </div>
  <dialog class="fixed inset-0 m-0 size-full max-h-none max-w-none border-0 bg-transparent px-3 pt-[68px] pb-[88px] text-white backdrop:bg-[rgba(14,11,10,.96)] open:flex open:animate-lb-in open:justify-center sm:px-20 sm:pt-16 sm:pb-7 motion-reduce:open:animate-none" data-lb-dialog aria-labelledby="fd-cap">
    <button class="absolute grid size-[46px] cursor-pointer place-items-center rounded-full bg-white/14 text-white transition-colors duration-200 hover:bg-white/28 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#f3b9a6] motion-reduce:transition-none [&>svg]:size-5 top-3 right-4" type="button" data-lb-close aria-label="Close photo viewer" autofocus><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg></button>
    <figure class="flex size-full max-w-[1200px] flex-col gap-3.5"><img class="min-h-0 w-full flex-1 object-contain" data-lb-img alt=""><figcaption class="flex justify-between gap-4 text-[.92rem] text-[#f2ece8]"><span id="fd-cap" data-lb-cap></span><span class="flex-none text-[#cfc6c1] tabular-nums" data-lb-count></span></figcaption></figure>
    <button class="absolute grid size-[46px] cursor-pointer place-items-center rounded-full bg-white/14 text-white transition-colors duration-200 hover:bg-white/28 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#f3b9a6] motion-reduce:transition-none [&>svg]:size-5 bottom-5 left-[calc(50%-58px)] sm:top-1/2 sm:bottom-auto sm:left-4 sm:-mt-[23px]" type="button" data-lb-prev aria-label="Previous photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
    <button class="absolute grid size-[46px] cursor-pointer place-items-center rounded-full bg-white/14 text-white transition-colors duration-200 hover:bg-white/28 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#f3b9a6] motion-reduce:transition-none [&>svg]:size-5 right-[calc(50%-58px)] bottom-5 sm:top-1/2 sm:right-4 sm:bottom-auto sm:-mt-[23px]" type="button" data-lb-next aria-label="Next photo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 6l6 6-6 6"/></svg></button>
  </dialog>
</article></div>
CSS
@theme {
  --animate-lb-in: lb-in .25s ease-out;
  @keyframes lb-in { from { opacity: 0; transform: scale(.97); } }
}
JavaScript
// Lightbox: one <dialog> per [data-lightbox] group. Links keep their href, so without
// this script a click simply opens the full-size photo.
document.querySelectorAll('[data-lightbox]').forEach((root) => {
  const items = [...root.querySelectorAll('[data-lb-item]')];
  const dlg = root.querySelector('[data-lb-dialog]');
  const img = dlg.querySelector('[data-lb-img]');
  const cap = dlg.querySelector('[data-lb-cap]');
  const count = dlg.querySelector('[data-lb-count]');
  const arrows = dlg.querySelectorAll('[data-lb-prev], [data-lb-next]');
  let current = 0;
  const show = (n) => {
    current = (n + items.length) % items.length;
    const link = items[current];
    img.src = link.href;
    img.alt = link.querySelector('img').alt;
    cap.textContent = link.dataset.caption || '';
    count.textContent = items.length > 1 ? (current + 1) + ' of ' + items.length : '';
  };
  arrows.forEach((b) => { b.hidden = items.length < 2; });
  items.forEach((link, n) => link.addEventListener('click', (e) => {
    e.preventDefault();
    show(n);
    dlg.showModal();
  }));
  dlg.querySelector('[data-lb-prev]').addEventListener('click', () => show(current - 1));
  dlg.querySelector('[data-lb-next]').addEventListener('click', () => show(current + 1));
  dlg.querySelector('[data-lb-close]').addEventListener('click', () => dlg.close());
  dlg.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowLeft') show(current - 1);
    if (e.key === 'ArrowRight') show(current + 1);
  });
  dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });  // click on the dark area
  dlg.addEventListener('close', () => items[current].focus());                   // back to the photo you were on
});

About this set

Two lightbox galleries that open a photo large over the page. Photo grid that opens a lightbox shows six wedding photographs as 4:3 thumbnails with an enlarge icon; activating one opens a full-screen viewer with the photo, its caption, a 2 of 6 counter, previous and next buttons and a close button. Article images that open full screen puts two wildlife photos inside a short field diary entry, the click-to-enlarge pattern for blog posts and case studies, using the same viewer. The plain CSS and Tailwind versions use the native dialog element with showModal(), so the browser traps focus inside it and closes it on Esc; a short vanilla script fill in the photo, step through the set with the arrow keys or buttons, close it on a click on the dark area and return focus to the thumbnail of the photo you were looking at. The Bootstrap version is a .modal-fullscreen in the dark theme, opened with data-bs-toggle and filled from the thumbnail that opened it. Every thumbnail is a real link to the large photo, so without the script it still opens the picture.

What’s included

  • Full-screen viewer with caption, counter, arrows and a close button
  • Arrow keys step through the photos and Esc closes
  • Native <dialog> with showModal() in the CSS and Tailwind versions
  • Bootstrap version built on .modal-fullscreen with data-bs-theme="dark"
  • Thumbnails are links to the large photo, so they work without JavaScript

Accessibility

The viewer is a modal dialog named by the photo caption, focus moves into it on open and cannot leave it until it closes, and it closes with Esc, the labelled close button or a click on the dark area. Focus then returns to the thumbnail of the last photo shown. The large image gets the same descriptive alt text as its thumbnail, and every button has a name such as Next photo. The open animation and thumbnail zoom stop under prefers-reduced-motion.

Customise it

Change the backdrop colour in .lb-dialog::backdrop, the button colours in .lb-btn and the thumbnail columns in .lbg-grid. In Bootstrap restyle .lb-modal .modal-content; in Tailwind edit the backdrop: and bg-white/14 utilities on the dialog and its buttons.