Open demo

CSS code

HTML
<div><span class="lbl">Main photo with a thumbnail strip</span>
<section class="gal" role="region" aria-roledescription="carousel" aria-label="Alder House photos" data-carousel data-loop>
  <div class="gal-head"><div><p class="gal-kicker">For sale · Hampstead</p><h2>Alder House</h2></div><p class="gal-price">£1,850,000 <span>4 bed · 3 bath · 212 m²</span></p></div>
  <div class="gal-main">
    <div class="gal-track" data-track tabindex="0">
      <div class="gal-slide" role="group" aria-roledescription="slide" aria-label="1 of 5">
        <img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" alt="Two-storey timber and glass house lit from inside at dusk, with a lawn and a large tree">
        <span class="gal-count" aria-hidden="true">1 / 5</span><span class="gal-cap">Garden elevation at dusk</span>
      </div>
      <div class="gal-slide" role="group" aria-roledescription="slide" aria-label="2 of 5">
        <img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" loading="lazy" alt="House front with vertical timber cladding, black metal panels and a gravel drive">
        <span class="gal-count" aria-hidden="true">2 / 5</span><span class="gal-cap">Street front</span>
      </div>
      <div class="gal-slide" role="group" aria-roledescription="slide" aria-label="3 of 5">
        <img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" loading="lazy" alt="Open living room with a white sofa, a walnut feature wall and a glass wall to the garden">
        <span class="gal-count" aria-hidden="true">3 / 5</span><span class="gal-cap">Living room</span>
      </div>
      <div class="gal-slide" role="group" aria-roledescription="slide" aria-label="4 of 5">
        <img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" loading="lazy" alt="Bedroom with an upholstered bed, dark walls and a brass ceiling light">
        <span class="gal-count" aria-hidden="true">4 / 5</span><span class="gal-cap">Main bedroom</span>
      </div>
      <div class="gal-slide" role="group" aria-roledescription="slide" aria-label="5 of 5">
        <img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=1400&h=875&q=70" width="1400" height="875" loading="lazy" alt="White bathroom with a walk-in glass shower and a long vanity">
        <span class="gal-count" aria-hidden="true">5 / 5</span><span class="gal-cap">Family bathroom</span>
      </div>
    </div>
    <button class="gal-arrow" type="button" data-prev data-controls hidden 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="gal-arrow" type="button" data-next data-controls hidden 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 class="gal-thumbs" data-controls hidden>
    <button type="button" data-dot aria-label="Show photo 1: Garden elevation at dusk"><img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-dot aria-label="Show photo 2: Street front"><img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-dot aria-label="Show photo 3: Living room"><img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-dot aria-label="Show photo 4: Main bedroom"><img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-dot aria-label="Show photo 5: Family bathroom"><img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
  </div>
</section></div>

<div><span class="lbl">Side thumbnails</span>
<section class="stay" role="region" aria-roledescription="carousel" aria-label="Garden suite photos" data-carousel>
  <div class="stay-main">
    <div class="stay-track" data-track tabindex="0">
      <div class="stay-slide" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=1200&h=900&q=70" width="1200" height="900" alt="Infinity pool with cabanas and palm trees at dusk">
      </div>
      <div class="stay-slide" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=1200&h=900&q=70" width="1200" height="900" loading="lazy" alt="Hotel bedroom with a low wooden bed, a carved headboard and garden windows">
      </div>
      <div class="stay-slide" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=1200&h=900&q=70" width="1200" height="900" loading="lazy" alt="Bedroom with a wooden bench and tall doors open onto a terrace">
      </div>
      <div class="stay-slide" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=1200&h=900&q=70" width="1200" height="900" loading="lazy" alt="Hotel room with a wide bed, warm bedside lamps and a large headboard">
      </div>
    </div>
  </div>
  <div class="stay-thumbs" data-controls hidden>
      <button type="button" data-dot aria-label="Show photo 1: Infinity pool"><img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
      <button type="button" data-dot aria-label="Show photo 2: Garden suite"><img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
      <button type="button" data-dot aria-label="Show photo 3: Terrace doors"><img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
      <button type="button" data-dot aria-label="Show photo 4: Twin room"><img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
  </div>
</section></div>
CSS
/* Thumbnail galleries: the main image is a CSS scroll-snap track; the thumbnails are the dots.
   Without the script the photos still swipe and scroll, and the thumbnails stay hidden. */
[data-controls][hidden]{display:none}
.gal{--gal-ink:#14261f;--gal-muted:#56645e;--gal-accent:#1f4d3a;--gal-radius:18px}
.gal-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:8px 24px;margin-bottom:18px}
.gal-kicker{font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--gal-accent)}
.gal-head h2{font-size:1.7rem;letter-spacing:-.02em;margin-top:4px}
.gal-price{font-size:1.35rem;font-weight:700;text-align:right}
.gal-price span{display:block;margin-top:2px;font-size:.88rem;font-weight:500;color:var(--gal-muted)}
.gal-main{position:relative;border-radius:var(--gal-radius);overflow:hidden;background:#dfe5e2}
.gal-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.gal.is-ready .gal-track{scrollbar-width:none}
.gal-slide{position:relative;flex:0 0 100%;aspect-ratio:16/10;scroll-snap-align:start;scroll-snap-stop:always}
.gal-slide img{width:100%;height:100%;object-fit:cover;display:block}
.gal-count,.gal-cap{position:absolute;padding:6px 12px;border-radius:999px;background:rgba(10,20,16,.72);color:#fff;font-size:.8rem;font-weight:600}
.gal-count{top:14px;right:14px}.gal-cap{left:14px;bottom:14px}
.gal-arrow{position:absolute;z-index:1;top:50%;margin-top:-22px;width:44px;height:44px;display:grid;place-items:center;border:0;border-radius:50%;
  background:rgba(255,255,255,.94);color:var(--gal-ink);cursor:pointer;box-shadow:0 6px 18px rgba(0,0,0,.18);transition:transform .2s}
.gal-arrow:hover{transform:scale(1.06)}
.gal-arrow[data-prev]{left:14px}.gal-arrow[data-next]{right:14px}
.gal-arrow svg{width:18px;height:18px}
.gal-thumbs{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:10px}
.gal-thumbs button{position:relative;padding:0;border:0;border-radius:12px;overflow:hidden;background:none;cursor:pointer}
.gal-thumbs img{width:100%;height:72px;object-fit:cover;display:block;opacity:.62;transition:opacity .2s}
.gal-thumbs button::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 3px transparent;transition:box-shadow .2s}
.gal-thumbs button:hover img,.gal-thumbs button[aria-current=true] img{opacity:1}
.gal-thumbs button[aria-current=true]::after{box-shadow:inset 0 0 0 3px var(--gal-accent)}
.gal-track:focus-visible{outline:3px solid #fff;outline-offset:-6px}
.gal-arrow:focus-visible,.gal-thumbs button:focus-visible{outline:3px solid #d97706;outline-offset:3px}
@media (max-width:640px){.gal-thumbs{gap:6px}.gal-thumbs button{border-radius:8px}.gal-thumbs img{height:52px}.gal-price{text-align:left}.gal-head h2{font-size:1.45rem}
  .gal-arrow{width:38px;height:38px;margin-top:-19px}}

/* Side thumbnails: a column beside the photo that turns into a row on small screens */
.stay{--stay-accent:#9a3412;display:grid;grid-template-columns:96px 1fr;gap:12px}
.stay-main{grid-column:2;grid-row:1;border-radius:18px;overflow:hidden;background:#e7dfd8;min-width:0}
.stay-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.stay.is-ready .stay-track{scrollbar-width:none}
.stay-slide{flex:0 0 100%;aspect-ratio:4/3;scroll-snap-align:start;scroll-snap-stop:always}
.stay-slide img{width:100%;height:100%;object-fit:cover;display:block}
.stay-thumbs{grid-column:1;grid-row:1;display:flex;flex-direction:column;gap:12px}
.stay-thumbs button{position:relative;width:96px;height:96px;padding:0;border:0;border-radius:14px;overflow:hidden;background:none;cursor:pointer}
.stay-thumbs img{width:100%;height:100%;object-fit:cover;display:block;filter:grayscale(.6);opacity:.75;transition:filter .2s,opacity .2s}
.stay-thumbs button::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 3px transparent;transition:box-shadow .2s}
.stay-thumbs button:hover img,.stay-thumbs button[aria-current=true] img{filter:none;opacity:1}
.stay-thumbs button[aria-current=true]::after{box-shadow:inset 0 0 0 3px var(--stay-accent)}
.stay-track:focus-visible{outline:3px solid #fff;outline-offset:-6px}
.stay-thumbs button:focus-visible{outline:3px solid var(--stay-accent);outline-offset:3px}
@media (max-width:640px){.stay{grid-template-columns:1fr}.stay-main{grid-column:1}
  .stay-thumbs{grid-row:2;flex-direction:row}.stay-thumbs button{width:auto;height:auto;flex:1;aspect-ratio:1}}
@media (prefers-reduced-motion:reduce){.gal-arrow,.gal-thumbs img,.gal-thumbs button::after,.stay-thumbs img,.stay-thumbs button::after{transition:none}
  .gal-arrow:hover{transform:none}}
JavaScript
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
  const track = root.querySelector('[data-track]');
  const slides = [...track.children];
  const dots = [...root.querySelectorAll('[data-dot]')];
  const prev = root.querySelector('[data-prev]');
  const next = root.querySelector('[data-next]');
  const loop = root.hasAttribute('data-loop');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
  const current = () => Math.round(track.scrollLeft / step());
  const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
  const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
  const move = (dir) => {
    if (loop && dir > 0 && atEnd()) return go(0);
    if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
    const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
    go(current() + dir * perView);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
    if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
    if (!loop && next) next.setAttribute('aria-disabled', atEnd());
  };
  prev?.addEventListener('click', () => move(-1));
  next?.addEventListener('click', () => move(1));
  dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
  track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div><span class="lbl">Main photo with a thumbnail strip</span>
<section class="gal">
  <div class="gal-head d-flex flex-wrap align-items-end justify-content-between"><div><p class="gal-kicker">For sale · Hampstead</p><h2 class="gal-title">Alder House</h2></div><p class="gal-price">£1,850,000 <span>4 bed · 3 bath · 212 m²</span></p></div>
  <div id="alder" class="carousel slide" role="region" aria-roledescription="carousel" aria-label="Alder House photos">
    <div class="carousel-inner" aria-live="polite">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 5">
        <img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1400&h=875&q=70" class="d-block w-100" width="1400" height="875" alt="Two-storey timber and glass house lit from inside at dusk, with a lawn and a large tree">
        <span class="gal-count" aria-hidden="true">1 / 5</span><span class="gal-cap">Garden elevation at dusk</span>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 5">
        <img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=1400&h=875&q=70" class="d-block w-100" width="1400" height="875" alt="House front with vertical timber cladding, black metal panels and a gravel drive">
        <span class="gal-count" aria-hidden="true">2 / 5</span><span class="gal-cap">Street front</span>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 5">
        <img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1400&h=875&q=70" class="d-block w-100" width="1400" height="875" alt="Open living room with a white sofa, a walnut feature wall and a glass wall to the garden">
        <span class="gal-count" aria-hidden="true">3 / 5</span><span class="gal-cap">Living room</span>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 5">
        <img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=1400&h=875&q=70" class="d-block w-100" width="1400" height="875" alt="Bedroom with an upholstered bed, dark walls and a brass ceiling light">
        <span class="gal-count" aria-hidden="true">4 / 5</span><span class="gal-cap">Main bedroom</span>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="5 of 5">
        <img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=1400&h=875&q=70" class="d-block w-100" width="1400" height="875" alt="White bathroom with a walk-in glass shower and a long vanity">
        <span class="gal-count" aria-hidden="true">5 / 5</span><span class="gal-cap">Family bathroom</span>
      </div>
      <button class="carousel-control-prev" type="button" data-bs-target="#alder" data-bs-slide="prev"><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><span class="visually-hidden">Previous photo</span></button>
      <button class="carousel-control-next" type="button" data-bs-target="#alder" data-bs-slide="next"><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><span class="visually-hidden">Next photo</span></button>
    </div>
    <div class="carousel-indicators">
    <button type="button" data-bs-target="#alder" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show photo 1: Garden elevation at dusk"><img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-bs-target="#alder" data-bs-slide-to="1" aria-label="Show photo 2: Street front"><img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-bs-target="#alder" data-bs-slide-to="2" aria-label="Show photo 3: Living room"><img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-bs-target="#alder" data-bs-slide-to="3" aria-label="Show photo 4: Main bedroom"><img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    <button type="button" data-bs-target="#alder" data-bs-slide-to="4" aria-label="Show photo 5: Family bathroom"><img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=240&h=150&q=70" width="120" height="75" alt=""></button>
    </div>
  </div>
</section></div>

<div><span class="lbl">Side thumbnails</span>
<div id="suite" class="carousel slide stay" role="region" aria-roledescription="carousel" aria-label="Garden suite photos">
  <div class="carousel-inner" aria-live="polite">
        <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
          <img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=1200&h=900&q=70" class="d-block w-100" width="1200" height="900" alt="Infinity pool with cabanas and palm trees at dusk">
        </div>
        <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
          <img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=1200&h=900&q=70" class="d-block w-100" width="1200" height="900" alt="Hotel bedroom with a low wooden bed, a carved headboard and garden windows">
        </div>
        <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
          <img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=1200&h=900&q=70" class="d-block w-100" width="1200" height="900" alt="Bedroom with a wooden bench and tall doors open onto a terrace">
        </div>
        <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
          <img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=1200&h=900&q=70" class="d-block w-100" width="1200" height="900" alt="Hotel room with a wide bed, warm bedside lamps and a large headboard">
        </div>
  </div>
  <div class="carousel-indicators">
      <button type="button" data-bs-target="#suite" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show photo 1: Infinity pool"><img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
      <button type="button" data-bs-target="#suite" data-bs-slide-to="1" aria-label="Show photo 2: Garden suite"><img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
      <button type="button" data-bs-target="#suite" data-bs-slide-to="2" aria-label="Show photo 3: Terrace doors"><img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
      <button type="button" data-bs-target="#suite" data-bs-slide-to="3" aria-label="Show photo 4: Twin room"><img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=200&h=200&q=70" width="96" height="96" alt=""></button>
  </div>
</div></div>
CSS
/* Thumbnail galleries on Bootstrap's .carousel: the .carousel-indicators are restyled into
   thumbnail buttons, so Bootstrap keeps .active and aria-current on them in sync. */
.gal{--gal-ink:#14261f;--gal-muted:#56645e;--gal-accent:#1f4d3a;--gal-radius:18px}
.gal-head{gap:8px 24px;margin-bottom:18px}
.gal-kicker{margin:0;font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--gal-accent)}
.gal-title{margin:4px 0 0;font-size:1.7rem;font-weight:700;letter-spacing:-.02em}
.gal-price{margin:0;font-size:1.35rem;font-weight:700;text-align:right}
.gal-price span{display:block;margin-top:2px;font-size:.88rem;font-weight:500;color:var(--gal-muted)}
.gal .carousel-inner{border-radius:var(--gal-radius);background:#dfe5e2}
.gal .carousel-item img{aspect-ratio:16/10;height:auto;object-fit:cover}
.gal-count,.gal-cap{position:absolute;padding:6px 12px;border-radius:999px;background:rgba(10,20,16,.72);color:#fff;font-size:.8rem;font-weight:600}
.gal-count{top:14px;right:14px}.gal-cap{left:14px;bottom:14px}
.gal .carousel-control-prev,.gal .carousel-control-next{top:50%;bottom:auto;margin-top:-22px;width:44px;height:44px;border-radius:50%;
  background:rgba(255,255,255,.94);color:var(--gal-ink);opacity:1;box-shadow:0 6px 18px rgba(0,0,0,.18);transition:transform .2s}
.gal .carousel-control-prev{left:14px}.gal .carousel-control-next{right:14px}
.gal .carousel-control-prev:hover,.gal .carousel-control-next:hover{color:var(--gal-ink);transform:scale(1.06)}
.gal .carousel-control-prev svg,.gal .carousel-control-next svg{width:18px;height:18px}
.gal .carousel-indicators{position:static;display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin:10px 0 0}
.gal .carousel-indicators [data-bs-target],.stay .carousel-indicators [data-bs-target]{position:relative;width:auto;height:auto;margin:0;padding:0;border:0;
  border-radius:12px;overflow:hidden;background:none;opacity:1;text-indent:0}
.gal .carousel-indicators img{width:100%;height:72px;object-fit:cover;display:block;opacity:.62;transition:opacity .2s}
.gal .carousel-indicators [data-bs-target]::after,.stay .carousel-indicators [data-bs-target]::after{content:"";position:absolute;inset:0;border-radius:inherit;
  box-shadow:inset 0 0 0 3px transparent;transition:box-shadow .2s}
.gal .carousel-indicators [data-bs-target]:hover img,.gal .carousel-indicators .active img{opacity:1}
.gal .carousel-indicators .active::after{box-shadow:inset 0 0 0 3px var(--gal-accent)}
.gal .carousel-control-prev:focus-visible,.gal .carousel-control-next:focus-visible,.gal .carousel-indicators [data-bs-target]:focus-visible{outline:3px solid #d97706;outline-offset:3px}
@media (max-width:640px){.gal .carousel-indicators{gap:6px}.gal .carousel-indicators [data-bs-target]{border-radius:8px}.gal .carousel-indicators img{height:52px}.gal-price{text-align:left}.gal-title{font-size:1.45rem}
  .gal .carousel-control-prev,.gal .carousel-control-next{width:38px;height:38px;margin-top:-19px}}

/* Side thumbnails: a column beside the photo that turns into a row on small screens */
.stay{--stay-accent:#9a3412;display:grid;grid-template-columns:96px 1fr;gap:12px}
.stay .carousel-inner{grid-column:2;grid-row:1;border-radius:18px;background:#e7dfd8}
.stay .carousel-item img{aspect-ratio:4/3;height:auto;object-fit:cover}
.stay .carousel-indicators{position:static;grid-column:1;grid-row:1;flex-direction:column;justify-content:flex-start;gap:12px;margin:0}
.stay .carousel-indicators [data-bs-target]{width:96px;height:96px;flex:none;border-radius:14px}
.stay .carousel-indicators img{width:100%;height:100%;object-fit:cover;display:block;filter:grayscale(.6);opacity:.75;transition:filter .2s,opacity .2s}
.stay .carousel-indicators [data-bs-target]:hover img,.stay .carousel-indicators .active img{filter:none;opacity:1}
.stay .carousel-indicators .active::after{box-shadow:inset 0 0 0 3px var(--stay-accent)}
.stay .carousel-indicators [data-bs-target]:focus-visible{outline:3px solid var(--stay-accent);outline-offset:3px}
@media (max-width:640px){.stay{grid-template-columns:1fr}.stay .carousel-inner{grid-column:1}
  .stay .carousel-indicators{grid-column:1;grid-row:2;flex-direction:row}.stay .carousel-indicators [data-bs-target]{width:auto;height:auto;flex:1;aspect-ratio:1}}
@media (prefers-reduced-motion:reduce){.gal .carousel-control-prev,.gal .carousel-control-next,.gal .carousel-indicators img,.stay .carousel-indicators img,
  .gal .carousel-indicators [data-bs-target]::after,.stay .carousel-indicators [data-bs-target]::after{transition:none}
  .gal .carousel-control-prev:hover,.gal .carousel-control-next:hover{transform:none}}
JavaScript
// Create the instances up front so the arrow keys work before the first click. No autoplay (ride is off).
document.querySelectorAll('.carousel').forEach((el) => bootstrap.Carousel.getOrCreateInstance(el));

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-gal-muted uppercase">Main photo with a thumbnail strip</span>
<section role="region" aria-roledescription="carousel" aria-label="Alder House photos" data-carousel data-loop>
  <div class="mb-[18px] flex flex-wrap items-end justify-between gap-x-6 gap-y-2"><div><p class="text-xs font-bold tracking-[.12em] text-gal-accent uppercase">For sale · Hampstead</p><h2 class="mt-1 text-[1.45rem] font-bold tracking-[-.02em] sm:text-[1.7rem]">Alder House</h2></div><p class="text-[1.35rem] font-bold sm:text-right">£1,850,000 <span class="mt-0.5 block text-[.88rem] font-medium text-gal-muted">4 bed · 3 bath · 212 m²</span></p></div>
  <div class="relative overflow-hidden rounded-[18px] bg-[#dfe5e2]">
    <div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-6 focus-visible:outline-white" data-track tabindex="0">
      <div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="1 of 5">
        <img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" alt="Two-storey timber and glass house lit from inside at dusk, with a lawn and a large tree">
        <span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">1 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Garden elevation at dusk</span>
      </div>
      <div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="2 of 5">
        <img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="House front with vertical timber cladding, black metal panels and a gravel drive">
        <span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">2 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Street front</span>
      </div>
      <div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="3 of 5">
        <img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="Open living room with a white sofa, a walnut feature wall and a glass wall to the garden">
        <span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">3 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Living room</span>
      </div>
      <div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="4 of 5">
        <img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="Bedroom with an upholstered bed, dark walls and a brass ceiling light">
        <span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">4 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Main bedroom</span>
      </div>
      <div class="relative aspect-[16/10] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="5 of 5">
        <img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=1400&h=875&q=70" class="block size-full object-cover" width="1400" height="875" loading="lazy" alt="White bathroom with a walk-in glass shower and a long vanity">
        <span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white top-3.5 right-3.5" aria-hidden="true">5 / 5</span><span class="absolute rounded-full bg-[rgba(10,20,16,.72)] px-3 py-1.5 text-[.8rem] font-semibold text-white bottom-3.5 left-3.5">Family bathroom</span>
      </div>
    </div>
    <button class="absolute top-1/2 z-[1] -mt-[19px] grid size-[38px] cursor-pointer place-items-center rounded-full bg-white/94 text-gal-ink shadow-[0_6px_18px_rgba(0,0,0,.18)] transition-transform duration-200 hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] sm:-mt-[22px] sm:size-11 motion-reduce:transition-none motion-reduce:hover:scale-100 left-3.5" type="button" data-prev data-controls hidden aria-label="Previous photo"><svg class="size-[18px]" 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 top-1/2 z-[1] -mt-[19px] grid size-[38px] cursor-pointer place-items-center rounded-full bg-white/94 text-gal-ink shadow-[0_6px_18px_rgba(0,0,0,.18)] transition-transform duration-200 hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] sm:-mt-[22px] sm:size-11 motion-reduce:transition-none motion-reduce:hover:scale-100 right-3.5" type="button" data-next data-controls hidden aria-label="Next photo"><svg class="size-[18px]" 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 class="mt-2.5 grid grid-cols-5 gap-1.5 sm:gap-2.5" data-controls hidden>
    <button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 1: Garden elevation at dusk"><img src="https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
    <button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 2: Street front"><img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
    <button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 3: Living room"><img src="https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
    <button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 4: Main bedroom"><img src="https://images.unsplash.com/photo-1616594039964-ae9021a400a0?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
    <button class="group/t relative cursor-pointer overflow-hidden rounded-lg p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#d97706] aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-gal-accent)] sm:rounded-xl motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 5: Family bathroom"><img src="https://images.unsplash.com/photo-1584622650111-993a426fbf0a?auto=format&fit=crop&w=240&h=150&q=70" class="block h-[52px] w-full object-cover opacity-62 sm:h-[72px] transition-opacity duration-200 group-hover/t:opacity-100 group-aria-[current=true]/t:opacity-100 motion-reduce:transition-none" width="120" height="75" alt=""></button>
  </div>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-gal-muted uppercase">Side thumbnails</span>
<section class="grid gap-3 sm:grid-cols-[96px_1fr]" role="region" aria-roledescription="carousel" aria-label="Garden suite photos" data-carousel>
  <div class="min-w-0 overflow-hidden rounded-[18px] bg-[#e7dfd8] sm:col-start-2 sm:row-start-1">
    <div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-6 focus-visible:outline-white" data-track tabindex="0">
      <div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" alt="Infinity pool with cabanas and palm trees at dusk">
      </div>
      <div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" loading="lazy" alt="Hotel bedroom with a low wooden bed, a carved headboard and garden windows">
      </div>
      <div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" loading="lazy" alt="Bedroom with a wooden bench and tall doors open onto a terrace">
      </div>
      <div class="aspect-[4/3] shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=1200&h=900&q=70" class="block size-full object-cover" width="1200" height="900" loading="lazy" alt="Hotel room with a wide bed, warm bedside lamps and a large headboard">
      </div>
    </div>
  </div>
  <div class="flex gap-3 sm:col-start-1 sm:row-start-1 sm:flex-col" data-controls hidden>
      <button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 1: Infinity pool"><img src="https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
      <button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 2: Garden suite"><img src="https://images.unsplash.com/photo-1611892440504-42a792e24d32?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
      <button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 3: Terrace doors"><img src="https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
      <button class="group/t relative aspect-square flex-1 cursor-pointer overflow-hidden rounded-[14px] p-0 after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_0_0_3px_transparent] after:transition-shadow after:duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-stay-accent aria-[current=true]:after:shadow-[inset_0_0_0_3px_var(--color-stay-accent)] sm:size-24 sm:flex-none motion-reduce:after:transition-none" type="button" data-dot aria-label="Show photo 4: Twin room"><img src="https://images.unsplash.com/photo-1618773928121-c32242e63f39?auto=format&fit=crop&w=200&h=200&q=70" class="block size-full object-cover opacity-75 grayscale-60 transition-[filter,opacity] duration-200 group-hover/t:opacity-100 group-hover/t:grayscale-0 group-aria-[current=true]/t:opacity-100 group-aria-[current=true]/t:grayscale-0 motion-reduce:transition-none" width="96" height="96" alt=""></button>
  </div>
</section></div>
CSS
@theme {
  --color-gal-ink: #14261f;
  --color-gal-muted: #56645e;
  --color-gal-accent: #1f4d3a;
  --color-stay-accent: #9a3412;
}
JavaScript
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
  const track = root.querySelector('[data-track]');
  const slides = [...track.children];
  const dots = [...root.querySelectorAll('[data-dot]')];
  const prev = root.querySelector('[data-prev]');
  const next = root.querySelector('[data-next]');
  const loop = root.hasAttribute('data-loop');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
  const current = () => Math.round(track.scrollLeft / step());
  const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
  const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
  const move = (dir) => {
    if (loop && dir > 0 && atEnd()) return go(0);
    if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
    const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
    go(current() + dir * perView);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
    if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
    if (!loop && next) next.setAttribute('aria-disabled', atEnd());
  };
  prev?.addEventListener('click', () => move(-1));
  next?.addEventListener('click', () => move(1));
  dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
  track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

About this set

Photo galleries with a large image and thumbnails to pick from. Main photo with a thumbnail strip is a property listing: a heading with the price, a 16 by 10 photo with a caption and a 1 / 5 counter, arrows over the photo and a row of five thumbnails underneath. Side thumbnails is a hotel room gallery with a column of square thumbnails beside the photo, shown in greyscale until chosen, that turns into a row under the photo on phones. Use the strip for listings and product pages with many photos, and the side column where the page is wide and the photos are square or portrait. The thumbnails are buttons, and the current one gets a coloured inner ring and full opacity. In the plain CSS and Tailwind versions the photos are a scroll-snap track that still swipes when JavaScript is off, and the thumbnails only appear once the script has wired them. The Bootstrap version restyles .carousel-indicators into the thumbnails, so Bootstrap itself keeps the active state and aria-current up to date.

What’s included

  • Thumbnail buttons that jump straight to a photo
  • Strip below the photo or a column beside it that becomes a row on phones
  • Caption and 1 / 5 counter over each photo
  • Photos swipe without JavaScript in the CSS and Tailwind versions
  • Bootstrap version turns .carousel-indicators into thumbnails

Accessibility

Each gallery is a labelled region with aria-roledescription="carousel" and slides labelled "3 of 5". Every photo has alt text, and each thumbnail button is named after its photo, for example "Show photo 2: Street front", with aria-current on the one showing. Thumbnails and arrows get an amber or rust outline on keyboard focus, separate from the ring that marks the current photo, and transitions stop under prefers-reduced-motion.

Customise it

Change --gal-accent and --gal-radius on .gal and --stay-accent on .stay; the thumbnail height is the height on .gal-thumbs img and the column width is the 96px in .stay's grid. In Tailwind edit the --color-gal-* and --color-stay-accent theme values.