<div><span class="lbl">Full-width slideshow with dots and a counter</span>
<section class="ss" data-slideshow role="region" aria-roledescription="carousel" aria-labelledby="ss-title">
<div class="gh"><div><p class="gh-kicker">Above the Clouds</p><h2 id="ss-title">Six days on the high route</h2></div><p class="gh-meta">6 photographs · Jonas Weber</p></div>
<div class="ss-frame">
<div class="ss-track" data-track tabindex="0" aria-label="Photos, scroll sideways">
<figure class="ss-slide" role="group" aria-roledescription="slide" aria-label="1 of 6"><img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" alt="Snow-dusted peaks rising above a sea of cloud at sunset"><figcaption><strong>Above the cloud sea</strong><span>Ridge camp, 3,100 m</span></figcaption></figure>
<figure class="ss-slide" role="group" aria-roledescription="slide" aria-label="2 of 6"><img src="https://images.unsplash.com/photo-1454496522488-7a8e488e8606?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="A snow-covered summit with steep ridges under a clear blue sky"><figcaption><strong>Summit ridge</strong><span>Day four, 09:20</span></figcaption></figure>
<figure class="ss-slide" role="group" aria-roledescription="slide" aria-label="3 of 6"><img src="https://images.unsplash.com/photo-1508739773434-c26b3d09e071?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="A rocky peak glowing orange in the last sun above a dark valley"><figcaption><strong>Last light on the peak</strong><span>Pass road, 19:48</span></figcaption></figure>
<figure class="ss-slide" role="group" aria-roledescription="slide" aria-label="4 of 6"><img src="https://images.unsplash.com/photo-1500964757637-c85e8a162699?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="Pastel layers of mountain ridges under an orange and blue sky"><figcaption><strong>Ridges at dawn</strong><span>Hut terrace, 06:05</span></figcaption></figure>
<figure class="ss-slide" role="group" aria-roledescription="slide" aria-label="5 of 6"><img src="https://images.unsplash.com/photo-1505765050516-f72dcac9c60e?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="A snowy mountain peak poking through thick white cloud"><figcaption><strong>Through the cloud</strong><span>Cable car, 11:30</span></figcaption></figure>
<figure class="ss-slide" role="group" aria-roledescription="slide" aria-label="6 of 6"><img src="https://images.unsplash.com/photo-1490682143684-14369e18dce8?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="The sun breaking over a forested valley in orange haze"><figcaption><strong>Sunrise over the valley</strong><span>Descent, 06:40</span></figcaption></figure>
</div>
</div>
<div class="ss-bar" data-controls hidden>
<div class="ss-dots">
<button type="button" data-dot aria-label="Show photo 1: Above the cloud sea"></button>
<button type="button" data-dot aria-label="Show photo 2: Summit ridge"></button>
<button type="button" data-dot aria-label="Show photo 3: Last light on the peak"></button>
<button type="button" data-dot aria-label="Show photo 4: Ridges at dawn"></button>
<button type="button" data-dot aria-label="Show photo 5: Through the cloud"></button>
<button type="button" data-dot aria-label="Show photo 6: Sunrise over the valley"></button>
</div>
<div class="ss-nav"><span class="ss-count" data-count aria-hidden="true">01 / 06</span>
<button class="ss-btn" type="button" data-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="ss-btn" type="button" data-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>
</section></div>
<div><span class="lbl">Photo essay with a text panel</span>
<section class="es" data-slideshow role="region" aria-roledescription="carousel" aria-labelledby="es-title">
<div class="gh"><div><p class="gh-kicker">Photo essay</p><h2 id="es-title">Morning service at Café Lumen</h2></div><p class="gh-meta">4 chapters · Inès Garnier</p></div>
<div class="es-frame">
<div class="es-track" data-track tabindex="0" aria-label="Chapters, scroll sideways">
<article class="es-slide" role="group" aria-roledescription="slide" aria-label="1 of 4: Dialling in"><img src="https://images.unsplash.com/photo-1497935586351-b67a49e012bf?auto=format&fit=crop&w=1000&h=750&q=70" width="1000" height="750" alt="A wooden board with ground coffee, beans, an espresso tamper and a cup with heart latte art"><div class="es-text"><p class="es-time">06:30</p><h3>Dialling in</h3><p>Before the doors open, Ana pulls six test shots and tastes each one. The grinder moves two notches finer, and the seventh shot runs in twenty-eight seconds.</p></div></article>
<article class="es-slide" role="group" aria-roledescription="slide" aria-label="2 of 4: First regulars"><img src="https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=1000&h=750&q=70" width="1000" height="750" loading="lazy" alt="Three cups of latte art on a wooden table among potted plants"><div class="es-text"><p class="es-time">08:15</p><h3>First regulars</h3><p>The window table fills first. Three flat whites, one with oat milk, poured in under four minutes while the queue reaches the door.</p></div></article>
<article class="es-slide" role="group" aria-roledescription="slide" aria-label="3 of 4: Brunch rush"><img src="https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=1000&h=750&q=70" width="1000" height="750" loading="lazy" alt="Toast topped with salad leaves and halved boiled eggs on a dark plate"><div class="es-text"><p class="es-time">10:40</p><h3>Brunch rush</h3><p>Forty-two plates leave the pass in an hour. The egg toast outsells everything else on the menu, as it has every Saturday since spring.</p></div></article>
<article class="es-slide" role="group" aria-roledescription="slide" aria-label="4 of 4: Last orders"><img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=1000&h=750&q=70" width="1000" height="750" loading="lazy" alt="Four hands raising cups of hot and iced coffee together over a table"><div class="es-text"><p class="es-time">16:00</p><h3>Last orders</h3><p>The chairs go up at four. Staff coffee is the only drink of the day nobody pays for, and the only one nobody photographs.</p></div></article>
</div>
</div>
<div class="es-bar" data-controls hidden>
<div class="es-dots">
<button type="button" data-dot aria-label="Chapter 1: Dialling in"></button>
<button type="button" data-dot aria-label="Chapter 2: First regulars"></button>
<button type="button" data-dot aria-label="Chapter 3: Brunch rush"></button>
<button type="button" data-dot aria-label="Chapter 4: Last orders"></button>
</div>
<span class="es-count" data-count aria-hidden="true">01 / 04</span>
<button class="es-btn" type="button" data-prev aria-label="Previous chapter"><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="es-btn" type="button" data-next aria-label="Next chapter"><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>
</section></div>
CSS
/* Slideshows: the photos sit on a CSS scroll-snap track that swipes and scrolls with no script.
The script only reveals the dots and buttons, moves one slide per click and keeps the counter
and aria-current in sync. Nothing plays on its own. */
.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:#b45309}
.gh h2{margin-top:4px;font-size:1.75rem;line-height:1.15;letter-spacing:-.02em}
.gh-meta{font-size:.9rem;color:#57534e}
[data-controls][hidden]{display:none}
/* 1. Full-width slideshow */
.ss{--ss-accent:#b45309;--ss-dot:#8a817c}
.ss-frame{border-radius:18px;overflow:hidden;background:#d6d3d1}
.ss-track,.es-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;scrollbar-width:none}
.ss-track::-webkit-scrollbar,.es-track::-webkit-scrollbar{display:none}
.ss-slide{position:relative;flex:0 0 100%;margin:0;aspect-ratio:16/9;scroll-snap-align:start;scroll-snap-stop:always}
.ss-slide img{width:100%;height:100%;object-fit:cover;display:block}
.ss-slide figcaption{position:absolute;left:0;right:0;bottom:0;display:flex;flex-direction:column;gap:2px;padding:56px 24px 20px;
background:linear-gradient(to top,rgba(12,10,9,.8),rgba(12,10,9,0));color:#fff}
.ss-slide strong{font-size:1.3rem;letter-spacing:-.01em}
.ss-slide figcaption span{font-size:.85rem;color:#e7e5e4}
.ss-bar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:14px}
.ss-dots,.es-dots{display:flex;flex-wrap:wrap;gap:4px}
.ss-dots button,.es-dots button{width:28px;height:24px;padding:0;border:0;background:none;cursor:pointer;transition:width .3s}
.ss-dots button::before,.es-dots button::before{content:"";display:block;height:4px;border-radius:2px;background:var(--ss-dot,#8a817c);transition:background-color .3s}
.ss-dots button[aria-current=true]{width:46px}
.ss-dots button[aria-current=true]::before{background:var(--ss-accent)}
.ss-nav{display:flex;align-items:center;gap:10px}
.ss-count,.es-count{font-size:.9rem;font-weight:600;font-variant-numeric:tabular-nums;color:#57534e;margin-right:6px}
.ss-btn,.es-btn{display:grid;place-items:center;width:44px;height:44px;padding:0;border:1.5px solid #8a817c;border-radius:50%;background:#fff;color:#1c1917;cursor:pointer;transition:background-color .2s,color .2s,border-color .2s}
.ss-btn svg,.es-btn svg{width:18px;height:18px}
.ss-btn:hover,.es-btn:hover{background:#1c1917;border-color:#1c1917;color:#fff}
.ss-dots button:focus-visible,.es-dots button:focus-visible,.ss-btn:focus-visible,.es-btn:focus-visible{outline:3px solid #b45309;outline-offset:2px;border-radius:6px}
.ss-btn:focus-visible,.es-btn:focus-visible{border-radius:50%}
.ss-track:focus-visible,.es-track:focus-visible{outline:3px solid #fff;outline-offset:-6px}
@media (max-width:640px){.ss-slide{aspect-ratio:4/3}.ss-slide figcaption{padding:40px 16px 14px}.ss-slide strong{font-size:1.05rem}.ss-count{display:none}}
/* 2. Photo essay with a text panel */
.es{--es-accent:#b45309}
.es-frame{border-radius:18px;overflow:hidden;background:#efe6da}
.es-slide{display:grid;grid-template-columns:7fr 5fr;flex:0 0 100%;scroll-snap-align:start;scroll-snap-stop:always;background:#efe6da}
.es-slide img{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;display:block}
.es-text{display:flex;flex-direction:column;justify-content:center;padding:32px 36px}
.es-time{font-size:.8rem;font-weight:700;letter-spacing:.14em;color:#9a4508}
.es-text h3{margin:6px 0 12px;font-size:1.6rem;line-height:1.15;letter-spacing:-.02em}
.es-text p:last-child{font-size:.98rem;line-height:1.6;color:#44403c}
.es-bar{display:flex;align-items:center;gap:10px;margin-top:14px}
.es-dots{flex:1;flex-wrap:nowrap}
.es-dots button{flex:1;width:auto}
.es-dots button[aria-current=true]::before{background:var(--es-accent)}
@media (max-width:575.98px){.es-slide{grid-template-columns:1fr;grid-template-rows:auto 1fr}.es-text{justify-content:flex-start;padding:20px;min-height:236px}.es-text h3{font-size:1.3rem}}
@media (prefers-reduced-motion:reduce){.ss-dots button,.es-dots button,.ss-dots button::before,.es-dots button::before,.ss-btn,.es-btn{transition:none}}
JavaScript
// Scroll-snap slideshow: reveals the controls, moves one slide per click (wrapping at the ends),
// and keeps the dots (aria-current) and the counter in step with the scroll position.
document.querySelectorAll('[data-slideshow]').forEach((root) => {
const track = root.querySelector('[data-track]');
const slides = [...track.children];
const dots = [...root.querySelectorAll('[data-dot]')];
const count = root.querySelector('[data-count]');
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const pad = (n) => String(n).padStart(2, '0');
const index = () => Math.round(track.scrollLeft / track.clientWidth);
const go = (i) => {
const n = (i + slides.length) % slides.length;
track.scrollTo({ left: n * track.clientWidth, behavior: reduce.matches ? 'auto' : 'smooth' });
};
const sync = () => {
const i = index();
dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
if (count) count.textContent = pad(i + 1) + ' / ' + pad(slides.length);
};
root.querySelector('[data-prev]').addEventListener('click', () => go(index() - 1));
root.querySelector('[data-next]').addEventListener('click', () => go(index() + 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; });
sync();
});
<div><span class="lbl">Full-width slideshow with dots and a counter</span>
<section class="ss" aria-labelledby="ss-title">
<div class="gh d-flex flex-wrap align-items-end justify-content-between"><div><p class="gh-kicker">Above the Clouds</p><h2 id="ss-title" class="gh-title">Six days on the high route</h2></div><p class="gh-meta">6 photographs · Jonas Weber</p></div>
<div id="peaks" class="carousel slide" data-bs-interval="false" data-bs-touch="true" role="region" aria-roledescription="carousel" aria-labelledby="ss-title">
<div class="carousel-inner ss-frame">
<figure class="carousel-item m-0 active" role="group" aria-roledescription="slide" aria-label="1 of 6"><span class="ratio ratio-16x9 d-block"><img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1600&h=900&q=70" class="object-fit-cover" width="1600" height="900" alt="Snow-dusted peaks rising above a sea of cloud at sunset"></span><figcaption class="ss-cap"><strong>Above the cloud sea</strong><span>Ridge camp, 3,100 m</span></figcaption></figure>
<figure class="carousel-item m-0" role="group" aria-roledescription="slide" aria-label="2 of 6"><span class="ratio ratio-16x9 d-block"><img src="https://images.unsplash.com/photo-1454496522488-7a8e488e8606?auto=format&fit=crop&w=1600&h=900&q=70" class="object-fit-cover" width="1600" height="900" loading="lazy" alt="A snow-covered summit with steep ridges under a clear blue sky"></span><figcaption class="ss-cap"><strong>Summit ridge</strong><span>Day four, 09:20</span></figcaption></figure>
<figure class="carousel-item m-0" role="group" aria-roledescription="slide" aria-label="3 of 6"><span class="ratio ratio-16x9 d-block"><img src="https://images.unsplash.com/photo-1508739773434-c26b3d09e071?auto=format&fit=crop&w=1600&h=900&q=70" class="object-fit-cover" width="1600" height="900" loading="lazy" alt="A rocky peak glowing orange in the last sun above a dark valley"></span><figcaption class="ss-cap"><strong>Last light on the peak</strong><span>Pass road, 19:48</span></figcaption></figure>
<figure class="carousel-item m-0" role="group" aria-roledescription="slide" aria-label="4 of 6"><span class="ratio ratio-16x9 d-block"><img src="https://images.unsplash.com/photo-1500964757637-c85e8a162699?auto=format&fit=crop&w=1600&h=900&q=70" class="object-fit-cover" width="1600" height="900" loading="lazy" alt="Pastel layers of mountain ridges under an orange and blue sky"></span><figcaption class="ss-cap"><strong>Ridges at dawn</strong><span>Hut terrace, 06:05</span></figcaption></figure>
<figure class="carousel-item m-0" role="group" aria-roledescription="slide" aria-label="5 of 6"><span class="ratio ratio-16x9 d-block"><img src="https://images.unsplash.com/photo-1505765050516-f72dcac9c60e?auto=format&fit=crop&w=1600&h=900&q=70" class="object-fit-cover" width="1600" height="900" loading="lazy" alt="A snowy mountain peak poking through thick white cloud"></span><figcaption class="ss-cap"><strong>Through the cloud</strong><span>Cable car, 11:30</span></figcaption></figure>
<figure class="carousel-item m-0" role="group" aria-roledescription="slide" aria-label="6 of 6"><span class="ratio ratio-16x9 d-block"><img src="https://images.unsplash.com/photo-1490682143684-14369e18dce8?auto=format&fit=crop&w=1600&h=900&q=70" class="object-fit-cover" width="1600" height="900" loading="lazy" alt="The sun breaking over a forested valley in orange haze"></span><figcaption class="ss-cap"><strong>Sunrise over the valley</strong><span>Descent, 06:40</span></figcaption></figure>
</div>
<div class="ss-bar d-flex align-items-center justify-content-between">
<div class="carousel-indicators ss-dots">
<button type="button" data-bs-target="#peaks" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show photo 1: Above the cloud sea"></button>
<button type="button" data-bs-target="#peaks" data-bs-slide-to="1" aria-label="Show photo 2: Summit ridge"></button>
<button type="button" data-bs-target="#peaks" data-bs-slide-to="2" aria-label="Show photo 3: Last light on the peak"></button>
<button type="button" data-bs-target="#peaks" data-bs-slide-to="3" aria-label="Show photo 4: Ridges at dawn"></button>
<button type="button" data-bs-target="#peaks" data-bs-slide-to="4" aria-label="Show photo 5: Through the cloud"></button>
<button type="button" data-bs-target="#peaks" data-bs-slide-to="5" aria-label="Show photo 6: Sunrise over the valley"></button>
</div>
<div class="ss-nav d-flex align-items-center"><span class="ss-count" data-count aria-hidden="true">01 / 06</span>
<button class="ss-btn" type="button" data-bs-target="#peaks" data-bs-slide="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="ss-btn" type="button" data-bs-target="#peaks" data-bs-slide="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">Photo essay with a text panel</span>
<section class="es" aria-labelledby="es-title">
<div class="gh d-flex flex-wrap align-items-end justify-content-between"><div><p class="gh-kicker">Photo essay</p><h2 id="es-title" class="gh-title">Morning service at Café Lumen</h2></div><p class="gh-meta">4 chapters · Inès Garnier</p></div>
<div id="lumen" class="carousel slide" data-bs-interval="false" role="region" aria-roledescription="carousel" aria-labelledby="es-title">
<div class="carousel-inner es-frame">
<article class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4: Dialling in"><div class="row g-0 h-100"><div class="col-sm-7"><img src="https://images.unsplash.com/photo-1497935586351-b67a49e012bf?auto=format&fit=crop&w=1000&h=750&q=70" class="es-img object-fit-cover" width="1000" height="750" alt="A wooden board with ground coffee, beans, an espresso tamper and a cup with heart latte art"></div><div class="col-sm-5 es-text"><p class="es-time">06:30</p><h3>Dialling in</h3><p>Before the doors open, Ana pulls six test shots and tastes each one. The grinder moves two notches finer, and the seventh shot runs in twenty-eight seconds.</p></div></div></article>
<article class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4: First regulars"><div class="row g-0 h-100"><div class="col-sm-7"><img src="https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=1000&h=750&q=70" class="es-img object-fit-cover" width="1000" height="750" loading="lazy" alt="Three cups of latte art on a wooden table among potted plants"></div><div class="col-sm-5 es-text"><p class="es-time">08:15</p><h3>First regulars</h3><p>The window table fills first. Three flat whites, one with oat milk, poured in under four minutes while the queue reaches the door.</p></div></div></article>
<article class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4: Brunch rush"><div class="row g-0 h-100"><div class="col-sm-7"><img src="https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=1000&h=750&q=70" class="es-img object-fit-cover" width="1000" height="750" loading="lazy" alt="Toast topped with salad leaves and halved boiled eggs on a dark plate"></div><div class="col-sm-5 es-text"><p class="es-time">10:40</p><h3>Brunch rush</h3><p>Forty-two plates leave the pass in an hour. The egg toast outsells everything else on the menu, as it has every Saturday since spring.</p></div></div></article>
<article class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4: Last orders"><div class="row g-0 h-100"><div class="col-sm-7"><img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=1000&h=750&q=70" class="es-img object-fit-cover" width="1000" height="750" loading="lazy" alt="Four hands raising cups of hot and iced coffee together over a table"></div><div class="col-sm-5 es-text"><p class="es-time">16:00</p><h3>Last orders</h3><p>The chairs go up at four. Staff coffee is the only drink of the day nobody pays for, and the only one nobody photographs.</p></div></div></article>
</div>
<div class="es-bar d-flex align-items-center">
<div class="carousel-indicators es-dots">
<button type="button" data-bs-target="#lumen" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Chapter 1: Dialling in"></button>
<button type="button" data-bs-target="#lumen" data-bs-slide-to="1" aria-label="Chapter 2: First regulars"></button>
<button type="button" data-bs-target="#lumen" data-bs-slide-to="2" aria-label="Chapter 3: Brunch rush"></button>
<button type="button" data-bs-target="#lumen" data-bs-slide-to="3" aria-label="Chapter 4: Last orders"></button>
</div>
<span class="es-count" data-count aria-hidden="true">01 / 04</span>
<button class="es-btn" type="button" data-bs-target="#lumen" data-bs-slide="prev" aria-label="Previous chapter"><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="es-btn" type="button" data-bs-target="#lumen" data-bs-slide="next" aria-label="Next chapter"><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>
</section></div>
CSS
/* Slideshows on Bootstrap's .carousel: the indicators become the dots and the prev/next
buttons sit in a bar under the photo. data-bs-interval="false" means nothing plays on its own;
a few lines of script keep the 01 / 06 counter in step. */
.gh{gap:6px 24px;margin-bottom:20px}
.gh-kicker{margin:0;font-size:.75rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#b45309}
.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:#57534e}
/* Shared controls */
.ss-dots,.es-dots{position:static;flex-wrap:wrap;justify-content:flex-start;gap:4px;margin:0}
.ss-dots [data-bs-target],.es-dots [data-bs-target]{width:28px;height:24px;margin:0;padding:0;border:0;background:none;opacity:1;text-indent:0;flex:none;transition:width .3s}
.ss-dots [data-bs-target]::before,.es-dots [data-bs-target]::before{content:"";display:block;height:4px;border-radius:2px;background:#8a817c;transition:background-color .3s}
.ss-dots .active::before,.es-dots .active::before{background:#b45309}
.ss-count,.es-count{font-size:.9rem;font-weight:600;font-variant-numeric:tabular-nums;color:#57534e;margin-right:6px}
.ss-btn,.es-btn{display:grid;place-items:center;width:44px;height:44px;padding:0;border:1.5px solid #8a817c;border-radius:50%;background:#fff;color:#1c1917;cursor:pointer;transition:background-color .2s,color .2s,border-color .2s}
.ss-btn svg,.es-btn svg{width:18px;height:18px}
.ss-btn:hover,.es-btn:hover{background:#1c1917;border-color:#1c1917;color:#fff}
.ss-dots [data-bs-target]:focus-visible,.es-dots [data-bs-target]:focus-visible{outline:3px solid #b45309;outline-offset:2px;border-radius:6px}
.ss-btn:focus-visible,.es-btn:focus-visible{outline:3px solid #b45309;outline-offset:2px}
/* 1. Full-width slideshow */
.ss-frame{border-radius:18px;background:#d6d3d1}
.ss-cap{position:absolute;left:0;right:0;bottom:0;display:flex;flex-direction:column;gap:2px;padding:56px 24px 20px;
background:linear-gradient(to top,rgba(12,10,9,.8),rgba(12,10,9,0));color:#fff}
.ss-cap strong{font-size:1.3rem;letter-spacing:-.01em}
.ss-cap span{font-size:.85rem;color:#e7e5e4}
.ss-bar{gap:16px;margin-top:14px}
.ss-dots .active{width:46px}
.ss-nav{gap:10px}
@media (max-width:640px){.ss .ratio{--bs-aspect-ratio:75%}.ss-cap{padding:40px 16px 14px}.ss-cap strong{font-size:1.05rem}.ss-count{display:none}}
/* 2. Photo essay with a text panel */
.es-frame{border-radius:18px;background:#efe6da}
.es .carousel-item{background:#efe6da}
.es-img{display:block;width:100%;height:auto;aspect-ratio:4/3}
.es-text{display:flex;flex-direction:column;justify-content:center;padding:32px 36px}
.es-time{margin:0;font-size:.8rem;font-weight:700;letter-spacing:.14em;color:#9a4508}
.es-text h3{margin:6px 0 12px;font-size:1.6rem;font-weight:700;line-height:1.15;letter-spacing:-.02em}
.es-text p:last-child{margin:0;font-size:.98rem;line-height:1.6;color:#44403c}
.es-bar{gap:10px;margin-top:14px}
.es-dots{flex:1;flex-wrap:nowrap}
.es-dots [data-bs-target]{flex:1;width:auto}
@media (max-width:575.98px){.es-text{justify-content:flex-start;padding:20px;min-height:236px}.es-text h3{font-size:1.3rem}}
@media (prefers-reduced-motion:reduce){.ss-dots [data-bs-target],.es-dots [data-bs-target],.ss-dots [data-bs-target]::before,.es-dots [data-bs-target]::before,.ss-btn,.es-btn{transition:none}}
JavaScript
// Keep the 01 / 06 counter in step with Bootstrap's carousel.
document.querySelectorAll('.carousel').forEach((el) => {
const count = el.querySelector('[data-count]');
const total = el.querySelectorAll('.carousel-item').length;
const pad = (n) => String(n).padStart(2, '0');
el.addEventListener('slid.bs.carousel', (e) => { if (count) count.textContent = pad(e.to + 1) + ' / ' + pad(total); });
});
Tailwind 4 code
Requires: Tailwind CSS 4
HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Full-width slideshow with dots and a counter</span>
<section data-slideshow role="region" aria-roledescription="carousel" aria-labelledby="ss-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-[#b45309] uppercase">Above the Clouds</p><h2 id="ss-title" class="mt-1 text-[1.75rem] leading-[1.15] font-bold tracking-[-.02em]">Six days on the high route</h2></div><p class="text-[.9rem] leading-[normal] text-[#57534e]">6 photographs · Jonas Weber</p></div>
<div class="overflow-hidden rounded-[18px] bg-[#d6d3d1]">
<div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain [scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-6 focus-visible:outline-white" data-track tabindex="0" aria-label="Photos, scroll sideways">
<figure class="relative aspect-[4/3] shrink-0 basis-full snap-start snap-always sm:aspect-video" role="group" aria-roledescription="slide" aria-label="1 of 6"><img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1600&h=900&q=70" class="block size-full object-cover" width="1600" height="900" alt="Snow-dusted peaks rising above a sea of cloud at sunset"><figcaption class="absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-linear-to-t from-[rgba(12,10,9,.8)] to-[rgba(12,10,9,0)] px-4 pt-10 pb-3.5 text-white sm:px-6 sm:pt-14 sm:pb-5"><strong class="text-[1.05rem] tracking-[-.01em] sm:text-[1.3rem]">Above the cloud sea</strong><span class="text-[.85rem] text-[#e7e5e4]">Ridge camp, 3,100 m</span></figcaption></figure>
<figure class="relative aspect-[4/3] shrink-0 basis-full snap-start snap-always sm:aspect-video" role="group" aria-roledescription="slide" aria-label="2 of 6"><img src="https://images.unsplash.com/photo-1454496522488-7a8e488e8606?auto=format&fit=crop&w=1600&h=900&q=70" class="block size-full object-cover" width="1600" height="900" loading="lazy" alt="A snow-covered summit with steep ridges under a clear blue sky"><figcaption class="absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-linear-to-t from-[rgba(12,10,9,.8)] to-[rgba(12,10,9,0)] px-4 pt-10 pb-3.5 text-white sm:px-6 sm:pt-14 sm:pb-5"><strong class="text-[1.05rem] tracking-[-.01em] sm:text-[1.3rem]">Summit ridge</strong><span class="text-[.85rem] text-[#e7e5e4]">Day four, 09:20</span></figcaption></figure>
<figure class="relative aspect-[4/3] shrink-0 basis-full snap-start snap-always sm:aspect-video" role="group" aria-roledescription="slide" aria-label="3 of 6"><img src="https://images.unsplash.com/photo-1508739773434-c26b3d09e071?auto=format&fit=crop&w=1600&h=900&q=70" class="block size-full object-cover" width="1600" height="900" loading="lazy" alt="A rocky peak glowing orange in the last sun above a dark valley"><figcaption class="absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-linear-to-t from-[rgba(12,10,9,.8)] to-[rgba(12,10,9,0)] px-4 pt-10 pb-3.5 text-white sm:px-6 sm:pt-14 sm:pb-5"><strong class="text-[1.05rem] tracking-[-.01em] sm:text-[1.3rem]">Last light on the peak</strong><span class="text-[.85rem] text-[#e7e5e4]">Pass road, 19:48</span></figcaption></figure>
<figure class="relative aspect-[4/3] shrink-0 basis-full snap-start snap-always sm:aspect-video" role="group" aria-roledescription="slide" aria-label="4 of 6"><img src="https://images.unsplash.com/photo-1500964757637-c85e8a162699?auto=format&fit=crop&w=1600&h=900&q=70" class="block size-full object-cover" width="1600" height="900" loading="lazy" alt="Pastel layers of mountain ridges under an orange and blue sky"><figcaption class="absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-linear-to-t from-[rgba(12,10,9,.8)] to-[rgba(12,10,9,0)] px-4 pt-10 pb-3.5 text-white sm:px-6 sm:pt-14 sm:pb-5"><strong class="text-[1.05rem] tracking-[-.01em] sm:text-[1.3rem]">Ridges at dawn</strong><span class="text-[.85rem] text-[#e7e5e4]">Hut terrace, 06:05</span></figcaption></figure>
<figure class="relative aspect-[4/3] shrink-0 basis-full snap-start snap-always sm:aspect-video" role="group" aria-roledescription="slide" aria-label="5 of 6"><img src="https://images.unsplash.com/photo-1505765050516-f72dcac9c60e?auto=format&fit=crop&w=1600&h=900&q=70" class="block size-full object-cover" width="1600" height="900" loading="lazy" alt="A snowy mountain peak poking through thick white cloud"><figcaption class="absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-linear-to-t from-[rgba(12,10,9,.8)] to-[rgba(12,10,9,0)] px-4 pt-10 pb-3.5 text-white sm:px-6 sm:pt-14 sm:pb-5"><strong class="text-[1.05rem] tracking-[-.01em] sm:text-[1.3rem]">Through the cloud</strong><span class="text-[.85rem] text-[#e7e5e4]">Cable car, 11:30</span></figcaption></figure>
<figure class="relative aspect-[4/3] shrink-0 basis-full snap-start snap-always sm:aspect-video" role="group" aria-roledescription="slide" aria-label="6 of 6"><img src="https://images.unsplash.com/photo-1490682143684-14369e18dce8?auto=format&fit=crop&w=1600&h=900&q=70" class="block size-full object-cover" width="1600" height="900" loading="lazy" alt="The sun breaking over a forested valley in orange haze"><figcaption class="absolute inset-x-0 bottom-0 flex flex-col gap-0.5 bg-linear-to-t from-[rgba(12,10,9,.8)] to-[rgba(12,10,9,0)] px-4 pt-10 pb-3.5 text-white sm:px-6 sm:pt-14 sm:pb-5"><strong class="text-[1.05rem] tracking-[-.01em] sm:text-[1.3rem]">Sunrise over the valley</strong><span class="text-[.85rem] text-[#e7e5e4]">Descent, 06:40</span></figcaption></figure>
</div>
</div>
<div class="mt-3.5 flex items-center justify-between gap-4" data-controls hidden>
<div class="flex flex-wrap gap-1">
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none w-7 transition-[width] duration-300 aria-[current=true]:w-[46px]" type="button" data-dot aria-label="Show photo 1: Above the cloud sea"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none w-7 transition-[width] duration-300 aria-[current=true]:w-[46px]" type="button" data-dot aria-label="Show photo 2: Summit ridge"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none w-7 transition-[width] duration-300 aria-[current=true]:w-[46px]" type="button" data-dot aria-label="Show photo 3: Last light on the peak"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none w-7 transition-[width] duration-300 aria-[current=true]:w-[46px]" type="button" data-dot aria-label="Show photo 4: Ridges at dawn"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none w-7 transition-[width] duration-300 aria-[current=true]:w-[46px]" type="button" data-dot aria-label="Show photo 5: Through the cloud"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none w-7 transition-[width] duration-300 aria-[current=true]:w-[46px]" type="button" data-dot aria-label="Show photo 6: Sunrise over the valley"></button>
</div>
<div class="flex items-center gap-2.5"><span class="mr-1.5 hidden text-[.9rem] font-semibold text-[#57534e] tabular-nums sm:inline" data-count aria-hidden="true">01 / 06</span>
<button class="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#8a817c] bg-white text-[#1c1917] transition-colors duration-200 hover:border-[#1c1917] hover:bg-[#1c1917] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none [&>svg]:size-[18px]" type="button" data-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="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#8a817c] bg-white text-[#1c1917] transition-colors duration-200 hover:border-[#1c1917] hover:bg-[#1c1917] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none [&>svg]:size-[18px]" type="button" data-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>
</section></div>
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#57534e] uppercase">Photo essay with a text panel</span>
<section data-slideshow role="region" aria-roledescription="carousel" aria-labelledby="es-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-[#b45309] uppercase">Photo essay</p><h2 id="es-title" class="mt-1 text-[1.75rem] leading-[1.15] font-bold tracking-[-.02em]">Morning service at Café Lumen</h2></div><p class="text-[.9rem] leading-[normal] text-[#57534e]">4 chapters · Inès Garnier</p></div>
<div class="overflow-hidden rounded-[18px] bg-[#efe6da]">
<div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain [scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-6 focus-visible:outline-white" data-track tabindex="0" aria-label="Chapters, scroll sideways">
<article class="grid shrink-0 basis-full snap-start snap-always grid-rows-[auto_1fr] bg-[#efe6da] min-[576px]:grid-cols-[7fr_5fr] min-[576px]:grid-rows-1" role="group" aria-roledescription="slide" aria-label="1 of 4: Dialling in"><img src="https://images.unsplash.com/photo-1497935586351-b67a49e012bf?auto=format&fit=crop&w=1000&h=750&q=70" class="block aspect-[4/3] h-auto w-full object-cover" width="1000" height="750" alt="A wooden board with ground coffee, beans, an espresso tamper and a cup with heart latte art"><div class="flex min-h-[236px] flex-col p-5 min-[576px]:min-h-0 min-[576px]:justify-center min-[576px]:px-9 min-[576px]:py-8"><p class="text-[.8rem] font-bold tracking-[.14em] text-[#9a4508]">06:30</p><h3 class="mt-1.5 mb-3 text-[1.3rem] leading-[1.15] font-bold tracking-[-.02em] min-[576px]:text-[1.6rem]">Dialling in</h3><p class="text-[.98rem] leading-[1.6] text-[#44403c]">Before the doors open, Ana pulls six test shots and tastes each one. The grinder moves two notches finer, and the seventh shot runs in twenty-eight seconds.</p></div></article>
<article class="grid shrink-0 basis-full snap-start snap-always grid-rows-[auto_1fr] bg-[#efe6da] min-[576px]:grid-cols-[7fr_5fr] min-[576px]:grid-rows-1" role="group" aria-roledescription="slide" aria-label="2 of 4: First regulars"><img src="https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=1000&h=750&q=70" class="block aspect-[4/3] h-auto w-full object-cover" width="1000" height="750" loading="lazy" alt="Three cups of latte art on a wooden table among potted plants"><div class="flex min-h-[236px] flex-col p-5 min-[576px]:min-h-0 min-[576px]:justify-center min-[576px]:px-9 min-[576px]:py-8"><p class="text-[.8rem] font-bold tracking-[.14em] text-[#9a4508]">08:15</p><h3 class="mt-1.5 mb-3 text-[1.3rem] leading-[1.15] font-bold tracking-[-.02em] min-[576px]:text-[1.6rem]">First regulars</h3><p class="text-[.98rem] leading-[1.6] text-[#44403c]">The window table fills first. Three flat whites, one with oat milk, poured in under four minutes while the queue reaches the door.</p></div></article>
<article class="grid shrink-0 basis-full snap-start snap-always grid-rows-[auto_1fr] bg-[#efe6da] min-[576px]:grid-cols-[7fr_5fr] min-[576px]:grid-rows-1" role="group" aria-roledescription="slide" aria-label="3 of 4: Brunch rush"><img src="https://images.unsplash.com/photo-1482049016688-2d3e1b311543?auto=format&fit=crop&w=1000&h=750&q=70" class="block aspect-[4/3] h-auto w-full object-cover" width="1000" height="750" loading="lazy" alt="Toast topped with salad leaves and halved boiled eggs on a dark plate"><div class="flex min-h-[236px] flex-col p-5 min-[576px]:min-h-0 min-[576px]:justify-center min-[576px]:px-9 min-[576px]:py-8"><p class="text-[.8rem] font-bold tracking-[.14em] text-[#9a4508]">10:40</p><h3 class="mt-1.5 mb-3 text-[1.3rem] leading-[1.15] font-bold tracking-[-.02em] min-[576px]:text-[1.6rem]">Brunch rush</h3><p class="text-[.98rem] leading-[1.6] text-[#44403c]">Forty-two plates leave the pass in an hour. The egg toast outsells everything else on the menu, as it has every Saturday since spring.</p></div></article>
<article class="grid shrink-0 basis-full snap-start snap-always grid-rows-[auto_1fr] bg-[#efe6da] min-[576px]:grid-cols-[7fr_5fr] min-[576px]:grid-rows-1" role="group" aria-roledescription="slide" aria-label="4 of 4: Last orders"><img src="https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?auto=format&fit=crop&w=1000&h=750&q=70" class="block aspect-[4/3] h-auto w-full object-cover" width="1000" height="750" loading="lazy" alt="Four hands raising cups of hot and iced coffee together over a table"><div class="flex min-h-[236px] flex-col p-5 min-[576px]:min-h-0 min-[576px]:justify-center min-[576px]:px-9 min-[576px]:py-8"><p class="text-[.8rem] font-bold tracking-[.14em] text-[#9a4508]">16:00</p><h3 class="mt-1.5 mb-3 text-[1.3rem] leading-[1.15] font-bold tracking-[-.02em] min-[576px]:text-[1.6rem]">Last orders</h3><p class="text-[.98rem] leading-[1.6] text-[#44403c]">The chairs go up at four. Staff coffee is the only drink of the day nobody pays for, and the only one nobody photographs.</p></div></article>
</div>
</div>
<div class="mt-3.5 flex items-center gap-2.5" data-controls hidden>
<div class="flex flex-1 gap-1">
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none flex-1" type="button" data-dot aria-label="Chapter 1: Dialling in"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none flex-1" type="button" data-dot aria-label="Chapter 2: First regulars"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none flex-1" type="button" data-dot aria-label="Chapter 3: Brunch rush"></button>
<button class="h-6 cursor-pointer before:block before:h-1 before:rounded-[2px] before:bg-[#8a817c] before:transition-colors before:duration-300 aria-[current=true]:before:bg-[#b45309] focus-visible:rounded-md focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none motion-reduce:before:transition-none flex-1" type="button" data-dot aria-label="Chapter 4: Last orders"></button>
</div>
<span class="mr-1.5 text-[.9rem] font-semibold text-[#57534e] tabular-nums" data-count aria-hidden="true">01 / 04</span>
<button class="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#8a817c] bg-white text-[#1c1917] transition-colors duration-200 hover:border-[#1c1917] hover:bg-[#1c1917] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none [&>svg]:size-[18px]" type="button" data-prev aria-label="Previous chapter"><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="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#8a817c] bg-white text-[#1c1917] transition-colors duration-200 hover:border-[#1c1917] hover:bg-[#1c1917] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#b45309] motion-reduce:transition-none [&>svg]:size-[18px]" type="button" data-next aria-label="Next chapter"><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>
</section></div>
JavaScript
// Scroll-snap slideshow: reveals the controls, moves one slide per click (wrapping at the ends),
// and keeps the dots (aria-current) and the counter in step with the scroll position.
document.querySelectorAll('[data-slideshow]').forEach((root) => {
const track = root.querySelector('[data-track]');
const slides = [...track.children];
const dots = [...root.querySelectorAll('[data-dot]')];
const count = root.querySelector('[data-count]');
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const pad = (n) => String(n).padStart(2, '0');
const index = () => Math.round(track.scrollLeft / track.clientWidth);
const go = (i) => {
const n = (i + slides.length) % slides.length;
track.scrollTo({ left: n * track.clientWidth, behavior: reduce.matches ? 'auto' : 'smooth' });
};
const sync = () => {
const i = index();
dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
if (count) count.textContent = pad(i + 1) + ' / ' + pad(slides.length);
};
root.querySelector('[data-prev]').addEventListener('click', () => go(index() - 1));
root.querySelector('[data-next]').addEventListener('click', () => go(index() + 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; });
sync();
});
About this set
Two slideshows that show one photo at a time and never play on their own. Full-width slideshow with dots and a counter is a six-photo mountain series in a 16:9 frame with the caption over a dark gradient; underneath sit pill-shaped dots, with the current one longer and coloured, a 01 / 06 counter and round previous and next buttons. Photo essay with a text panel pairs each of four café photographs with a chapter time, a heading and a paragraph on a sand-coloured panel, and uses four progress segments as its dots; on phones the text drops under the photo. In the plain CSS and Tailwind versions the slides are a CSS scroll-snap track, so they swipe and scroll without a script; a few lines of vanilla JavaScript reveal the controls, move one slide per click, wrap round at the ends and keep the dots and counter in step. The Bootstrap version is the native .carousel with data-bs-interval set to false, its indicators restyled into the dots and its controls moved into the bar under the photo.
What’s included
Scroll-snap slides that swipe without JavaScript
Dots, a 01 / 06 counter and previous and next buttons
Photo essay layout with a text panel per slide
No autoplay, and the last slide leads back to the first
Bootstrap version uses the native .carousel and .carousel-indicators
Accessibility
Each slideshow is a region with aria-roledescription="carousel" named by its heading, and each slide is a group labelled 2 of 6. The dots and arrows are buttons with names such as Show photo 3: Last light on the peak, the current dot has aria-current, and all controls show a 3px outline on focus. Nothing changes slide on its own, and under prefers-reduced-motion slides jump instead of scrolling smoothly.
Customise it
Change --ss-accent and --ss-dot on .ss and --es-accent on .es, the aspect-ratio on .ss-slide and the panel colour on .es-slide. In Bootstrap edit the colours in .ss-dots and .ss-btn; in Tailwind edit the aria-[current=true]:before:bg-[#b45309] and aspect-video utilities.