Open demo

CSS code

HTML
<div><span class="lbl">Autoplay with story-style progress</span>
<section class="pr" role="region" aria-roledescription="carousel" aria-label="Autumn collection" data-carousel data-loop>
  <div class="pr-track" data-track>
    <div class="pr-slide" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <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">
      <div class="pr-cap"><span class="pr-kicker">Autumn '26 · Shell layers</span><h2>Built for the weather above the weather</h2><p>The Ridgeline Storm shell weighs 310 g, packs into its own pocket and keeps a four-hour downpour out.</p><a class="pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="pr-slide" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="A walker on a rock outcrop above a green valley at sunrise">
      <div class="pr-cap"><span class="pr-kicker">Autumn '26 · Insulation</span><h2>Warm when you stop, dry when you don't</h2><p>Recycled synthetic fill that keeps its loft wet. The Hollow vest goes under a shell or over a base layer.</p><a class="pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="pr-slide" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="Pine forest and a wide glacial valley below snowy mountains">
      <div class="pr-cap"><span class="pr-kicker">Autumn '26 · Footwear</span><h2>A boot that is broken in on day one</h2><p>The Cairn mid uses a sock-fit liner and a Vibram sole. Three widths, half sizes from 36 to 48.</p><a class="pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="pr-slide" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <img src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=1600&h=900&q=70" width="1600" height="900" loading="lazy" alt="Sunlight falling across a path through a tall pine forest">
      <div class="pr-cap"><span class="pr-kicker">Autumn '26 · Repairs</span><h2>Send it back and we will fix it</h2><p>Free repairs for the life of every Ridgeline jacket. Last year our workshop mended 4,212 of them.</p><a class="pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
  </div>
  <div class="pr-segs" data-controls hidden>
    <button type="button" data-dot aria-label="Show slide 1"><i></i></button>
    <button type="button" data-dot aria-label="Show slide 2"><i></i></button>
    <button type="button" data-dot aria-label="Show slide 3"><i></i></button>
    <button type="button" data-dot aria-label="Show slide 4"><i></i></button>
  </div>
  <button class="pr-play" type="button" data-play data-controls hidden aria-label="Pause slideshow"><svg class="ic-pause" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><svg class="ic-play" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg></button>
</section></div>

<div><span class="lbl">Manual slides with a counter and progress line</span>
<section class="essay" role="region" aria-roledescription="carousel" aria-label="Places we walked this year" data-carousel data-loop>
  <div class="essay-track" data-track tabindex="0">
    <div class="essay-slide" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <img src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1600&h=760&q=70" width="1600" height="760" alt="Green cliffs and a winding road under a cloudy sunrise">
      <div class="essay-cap"><h3>Quiraing, Isle of Skye</h3><p>We started at 5 am to have the ridge to ourselves. The cloud lifted off the Trotternish cliffs just as the sun cleared the sea.</p></div>
    </div>
    <div class="essay-slide" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <img src="https://images.unsplash.com/photo-1511884642898-4c92249e20b6?auto=format&fit=crop&w=1600&h=760&q=70" width="1600" height="760" loading="lazy" alt="A river running through a misty pine forest in a steep valley">
      <div class="essay-cap"><h3>Glen Etive, Highlands</h3><p>Two days of rain turned the burn into a torrent. We waited it out under the pines and crossed on the third morning.</p></div>
    </div>
    <div class="essay-slide" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1600&h=760&q=70" width="1600" height="760" loading="lazy" alt="A road running between red sandstone cliffs">
      <div class="essay-cap"><h3>Valley of Fire, Nevada</h3><p>Twenty miles of red sandstone and not a single tree. We walked at dawn and dusk and slept through the heat.</p></div>
    </div>
  </div>
  <div class="essay-line"></div>
  <div class="essay-bar" data-controls hidden>
    <p class="essay-count" aria-hidden="true"><span data-count>01</span> / 03</p>
    <div class="essay-nav"><button type="button" data-prev aria-label="Previous place"><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 type="button" data-next aria-label="Next place"><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
/* Full-width slides with progress. The first autoplays: each segment's fill is a CSS animation and the
   script moves on when it ends, so pausing the animation (hover, focus, the button) pauses the carousel. */
[data-controls][hidden]{display:none}
.pr{--pr-interval:6s;--pr-accent:#fbbf24;position:relative;border-radius:20px;overflow:hidden;background:#15201b;color:#fff}
.pr-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.pr.is-ready .pr-track{scrollbar-width:none}
.pr-slide{position:relative;flex:0 0 100%;height:clamp(460px,50vw,560px);scroll-snap-align:start;scroll-snap-stop:always}
.pr-slide img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.pr-slide::after{content:"";position:absolute;inset:0;background:linear-gradient(100deg,rgba(10,16,13,.85),rgba(10,16,13,.55) 45%,rgba(10,16,13,.05) 80%)}
.pr-cap{position:absolute;z-index:1;left:clamp(22px,6vw,64px);right:22px;bottom:clamp(84px,9vw,96px);max-width:500px}
.pr-kicker{font-size:.74rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pr-accent)}
.pr-cap h2{margin:.5rem 0 .6rem;font-size:clamp(1.7rem,3.6vw,2.7rem);line-height:1.08;letter-spacing:-.025em}
.pr-cap p{line-height:1.55;color:rgba(255,255,255,.9)}
.pr-btn{display:inline-flex;align-items:center;gap:.5rem;margin-top:1.2rem;padding:.7rem 1.2rem;border-radius:10px;background:var(--pr-accent);
  color:#15201b;font-size:.9rem;font-weight:700;text-decoration:none;transition:background .2s}
.pr-btn:hover{background:#fff}
.pr-btn svg{width:16px;height:16px}
.pr-segs{position:absolute;z-index:2;top:14px;left:clamp(22px,6vw,64px);right:clamp(22px,6vw,64px);display:flex;gap:6px}
.pr-segs button{flex:1;height:24px;padding:0;border:0;background:none;cursor:pointer}
.pr-segs i{display:block;height:4px;border-radius:2px;background:rgba(255,255,255,.4);overflow:hidden}
.pr-segs i::before{content:"";display:block;height:100%;background:#fff;transform:scaleX(0);transform-origin:left}
.pr-segs [data-done] i::before{transform:none}
.pr-segs [aria-current=true] i::before{animation:pr-fill var(--pr-interval) linear forwards}
.pr:hover .pr-segs i::before,.pr:focus-within .pr-segs i::before,.pr.is-paused .pr-segs i::before{animation-play-state:paused}
@keyframes pr-fill{to{transform:none}}
.pr-play{position:absolute;z-index:2;left:clamp(22px,6vw,64px);bottom:26px;width:44px;height:44px;display:grid;place-items:center;
  border:1.5px solid rgba(255,255,255,.7);border-radius:50%;background:rgba(10,16,13,.35);color:#fff;cursor:pointer}
.pr-play svg{width:18px;height:18px}
.pr-play .ic-play,.pr.is-paused .pr-play .ic-pause{display:none}
.pr.is-paused .pr-play .ic-play{display:block}
.pr-btn:focus-visible,.pr-segs button:focus-visible,.pr-play:focus-visible{outline:3px solid var(--pr-accent);outline-offset:3px}

/* Manual slides with a counter and a progress line */
.essay{--essay-accent:#c2410c;position:relative;border-radius:20px;overflow:hidden;background:#fff;box-shadow:0 18px 40px rgba(26,31,28,.08)}
.essay-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.essay.is-ready .essay-track{scrollbar-width:none}
.essay-slide{flex:0 0 100%;scroll-snap-align:start;scroll-snap-stop:always}
.essay-slide img{width:100%;height:auto;aspect-ratio:16/7;object-fit:cover;display:block}
.essay-cap{padding:26px 250px 30px 32px}
.essay-cap h3{font-size:1.3rem;letter-spacing:-.015em}
.essay-cap p{margin-top:8px;line-height:1.6;color:#4a524d;max-width:60ch}
.essay-line{position:absolute;left:0;right:0;bottom:0;height:4px;background:#e7e5e0}
.essay-line::before{content:"";position:absolute;inset:0;background:var(--essay-accent);transform:scaleX(var(--progress,.333));transform-origin:left;transition:transform .4s}
.essay-bar{position:absolute;right:32px;bottom:34px;display:flex;align-items:center;gap:18px}
.essay-count{font-variant-numeric:tabular-nums;font-size:.95rem;font-weight:600;color:#4a524d}
.essay-count span{font-size:1.6rem;font-weight:800;color:#1a1f1c}
.essay-bar button{width:44px;height:44px;display:grid;place-items:center;border:1.5px solid #1a1f1c;border-radius:50%;background:none;color:#1a1f1c;cursor:pointer;transition:background .2s,color .2s}
.essay-bar button:hover{background:#1a1f1c;color:#fff}
.essay-bar svg{width:18px;height:18px}
.essay-bar button:focus-visible{outline:3px solid var(--essay-accent);outline-offset:3px}
.essay-nav{display:flex;gap:8px}
@media (max-width:640px){
  .pr-play{bottom:22px}.pr-cap{bottom:82px}
  .essay-slide img{aspect-ratio:4/3}.essay-cap{padding:22px 22px 92px}.essay-bar{left:22px;right:22px;bottom:26px;justify-content:space-between}}
@media (prefers-reduced-motion:reduce){
  .pr-segs [aria-current=true] i::before{animation:none;transform:none}
  .pr-play{display:none}.pr-cap{bottom:56px}
  .pr-btn,.essay-line::before,.essay-bar button{transition:none}}
JavaScript
// Scroll-snap carousel with an optional autoplay. The track scrolls and swipes without this script.
// Autoplay: the active progress bar is a CSS animation, and when it ends the carousel moves on.
// Hover, keyboard focus inside the carousel and the pause button pause that animation, and
// prefers-reduced-motion removes it, so under reduced motion nothing advances on its own.
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 count = root.querySelector('[data-count]');
  const play = root.querySelector('[data-play]');
  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);
    go(current() + dir);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => { dot.setAttribute('aria-current', n === i); dot.toggleAttribute('data-done', n < i); });
    if (count) count.textContent = String(i + 1).padStart(2, '0');
    root.style.setProperty('--progress', (i + 1) / slides.length);
  };
  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.addEventListener('animationend', (e) => {
    if (e.target.closest('[data-dot]')?.getAttribute('aria-current') === 'true') move(1);
  });
  play?.addEventListener('click', () => {
    const paused = root.classList.toggle('is-paused');
    play.setAttribute('aria-label', paused ? 'Play slideshow' : 'Pause slideshow');
  });
  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">Autoplay with story-style progress</span>
<div id="autumn" class="carousel slide pr" role="region" aria-roledescription="carousel" aria-label="Autumn collection">
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#autumn" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show slide 1"><i></i></button>
    <button type="button" data-bs-target="#autumn" data-bs-slide-to="1" aria-label="Show slide 2"><i></i></button>
    <button type="button" data-bs-target="#autumn" data-bs-slide-to="2" aria-label="Show slide 3"><i></i></button>
    <button type="button" data-bs-target="#autumn" data-bs-slide-to="3" aria-label="Show slide 4"><i></i></button>
  </div>
  <div class="carousel-inner">
    <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1600&h=900&q=70" class="d-block w-100" width="1600" height="900" alt="Snow-dusted peaks rising above a sea of cloud at sunset">
      <div class="carousel-caption"><span class="pr-kicker">Autumn '26 · Shell layers</span><h2>Built for the weather above the weather</h2><p>The Ridgeline Storm shell weighs 310 g, packs into its own pocket and keeps a four-hour downpour out.</p><a class="btn pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1600&h=900&q=70" class="d-block w-100" width="1600" height="900" alt="A walker on a rock outcrop above a green valley at sunrise">
      <div class="carousel-caption"><span class="pr-kicker">Autumn '26 · Insulation</span><h2>Warm when you stop, dry when you don't</h2><p>Recycled synthetic fill that keeps its loft wet. The Hollow vest goes under a shell or over a base layer.</p><a class="btn pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1600&h=900&q=70" class="d-block w-100" width="1600" height="900" alt="Pine forest and a wide glacial valley below snowy mountains">
      <div class="carousel-caption"><span class="pr-kicker">Autumn '26 · Footwear</span><h2>A boot that is broken in on day one</h2><p>The Cairn mid uses a sock-fit liner and a Vibram sole. Three widths, half sizes from 36 to 48.</p><a class="btn pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <img src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=1600&h=900&q=70" class="d-block w-100" width="1600" height="900" alt="Sunlight falling across a path through a tall pine forest">
      <div class="carousel-caption"><span class="pr-kicker">Autumn '26 · Repairs</span><h2>Send it back and we will fix it</h2><p>Free repairs for the life of every Ridgeline jacket. Last year our workshop mended 4,212 of them.</p><a class="btn pr-btn" href="#">Shop the range <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="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
  </div>
  <button class="btn rounded-circle pr-play" type="button" data-play aria-label="Pause slideshow"><svg class="ic-pause" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><svg class="ic-play" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg></button>
</div></div>

<div><span class="lbl">Manual slides with a counter and progress line</span>
<div id="places" class="carousel slide essay" role="region" aria-roledescription="carousel" aria-label="Places we walked this year">
  <div class="carousel-inner" aria-live="polite">
    <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <img src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1600&h=760&q=70" class="d-block w-100" width="1600" height="760" alt="Green cliffs and a winding road under a cloudy sunrise">
      <div class="essay-cap"><h3>Quiraing, Isle of Skye</h3><p>We started at 5 am to have the ridge to ourselves. The cloud lifted off the Trotternish cliffs just as the sun cleared the sea.</p></div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <img src="https://images.unsplash.com/photo-1511884642898-4c92249e20b6?auto=format&fit=crop&w=1600&h=760&q=70" class="d-block w-100" width="1600" height="760" alt="A river running through a misty pine forest in a steep valley">
      <div class="essay-cap"><h3>Glen Etive, Highlands</h3><p>Two days of rain turned the burn into a torrent. We waited it out under the pines and crossed on the third morning.</p></div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1600&h=760&q=70" class="d-block w-100" width="1600" height="760" alt="A road running between red sandstone cliffs">
      <div class="essay-cap"><h3>Valley of Fire, Nevada</h3><p>Twenty miles of red sandstone and not a single tree. We walked at dawn and dusk and slept through the heat.</p></div>
    </div>
  </div>
  <div class="essay-line"></div>
  <div class="essay-bar d-flex align-items-center">
    <p class="essay-count" aria-hidden="true"><span data-count>01</span> / 03</p>
    <div class="d-flex gap-2"><button class="btn rounded-circle" type="button" data-bs-target="#places" data-bs-slide="prev" aria-label="Previous place"><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="btn rounded-circle" type="button" data-bs-target="#places" data-bs-slide="next" aria-label="Next place"><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></div>
CSS
/* Full-width slides with progress on Bootstrap's .carousel. The indicators become progress segments;
   the script advances when the active segment's CSS animation ends (Bootstrap's own interval is not used). */
.pr{--pr-interval:6s;--pr-accent:#fbbf24;border-radius:20px;overflow:hidden;background:#15201b;color:#fff}
.pr .carousel-item img{height:clamp(460px,50vw,560px);object-fit:cover}
.pr .carousel-item::after{content:"";position:absolute;inset:0;background:linear-gradient(100deg,rgba(10,16,13,.85),rgba(10,16,13,.55) 45%,rgba(10,16,13,.05) 80%)}
.pr .carousel-caption{z-index:1;left:clamp(22px,6vw,64px);right:22px;bottom:clamp(84px,9vw,96px);max-width:500px;padding:0;text-align:left;color:#fff}
.pr-kicker{font-size:.74rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pr-accent)}
.pr .carousel-caption h2{margin:.5rem 0 .6rem;font-size:clamp(1.7rem,3.6vw,2.7rem);font-weight:700;line-height:1.08;letter-spacing:-.025em}
.pr .carousel-caption p{margin:0;line-height:1.55;color:rgba(255,255,255,.9)}
.pr-btn{--bs-btn-padding-x:1.2rem;--bs-btn-padding-y:.7rem;--bs-btn-font-size:.9rem;--bs-btn-font-weight:700;--bs-btn-line-height:normal;--bs-btn-border-width:0;
  --bs-btn-border-radius:10px;--bs-btn-bg:var(--pr-accent);--bs-btn-color:#15201b;--bs-btn-hover-bg:#fff;--bs-btn-hover-color:#15201b;--bs-btn-active-bg:#fff;--bs-btn-active-color:#15201b;
  display:inline-flex;align-items:center;gap:.5rem;margin-top:1.2rem}
.pr-btn svg{width:16px;height:16px}
.pr .carousel-indicators{top:14px;bottom:auto;left:clamp(22px,6vw,64px);right:clamp(22px,6vw,64px);margin:0;gap:6px}
.pr .carousel-indicators [data-bs-target]{flex:1;height:24px;margin:0;padding:0;border:0;background:none;opacity:1;text-indent:0}
.pr .carousel-indicators i{display:block;height:4px;border-radius:2px;background:rgba(255,255,255,.4);overflow:hidden}
.pr .carousel-indicators i::before{content:"";display:block;height:100%;background:#fff;transform:scaleX(0);transform-origin:left}
.pr .carousel-indicators [data-done] i::before{transform:none}
.pr .carousel-indicators .active i::before{animation:pr-fill var(--pr-interval) linear forwards}
.pr:hover .carousel-indicators i::before,.pr:focus-within .carousel-indicators i::before,.pr.is-paused .carousel-indicators i::before{animation-play-state:paused}
@keyframes pr-fill{to{transform:none}}
.pr-play{--bs-btn-color:#fff;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;--bs-btn-bg:rgba(10,16,13,.35);--bs-btn-hover-bg:rgba(10,16,13,.55);--bs-btn-active-bg:rgba(10,16,13,.55);
  --bs-btn-border-color:rgba(255,255,255,.7);--bs-btn-hover-border-color:#fff;--bs-btn-border-width:1.5px;
  position:absolute;z-index:2;left:clamp(22px,6vw,64px);bottom:26px;width:44px;height:44px;padding:0;display:grid;place-items:center}
.pr-play svg{width:18px;height:18px}
.pr-play .ic-play,.pr.is-paused .pr-play .ic-pause{display:none}
.pr.is-paused .pr-play .ic-play{display:block}
.pr-btn:focus-visible,.pr .carousel-indicators [data-bs-target]:focus-visible,.pr-play:focus-visible{outline:3px solid var(--pr-accent);outline-offset:3px;box-shadow:none}

/* Manual slides with a counter and a progress line */
.essay{--essay-accent:#c2410c;border-radius:20px;overflow:hidden;background:#fff;box-shadow:0 18px 40px rgba(26,31,28,.08)}
.essay .carousel-item img{aspect-ratio:16/7;height:auto;object-fit:cover}
.essay-cap{padding:26px 250px 30px 32px}
.essay-cap h3{margin:0;font-size:1.3rem;font-weight:700;letter-spacing:-.015em}
.essay-cap p{margin:8px 0 0;line-height:1.6;color:#4a524d;max-width:60ch}
.essay-line{position:absolute;left:0;right:0;bottom:0;height:4px;background:#e7e5e0}
.essay-line::before{content:"";position:absolute;inset:0;background:var(--essay-accent);transform:scaleX(var(--progress,.333));transform-origin:left;transition:transform .4s}
.essay-bar{position:absolute;z-index:2;right:32px;bottom:34px;gap:18px}
.essay-count{margin:0;font-variant-numeric:tabular-nums;font-size:.95rem;font-weight:600;color:#4a524d}
.essay-count span{font-size:1.6rem;font-weight:800;color:#1a1f1c}
.essay-bar .btn{--bs-btn-color:#1a1f1c;--bs-btn-border-color:#1a1f1c;--bs-btn-border-width:1.5px;--bs-btn-hover-bg:#1a1f1c;--bs-btn-hover-border-color:#1a1f1c;--bs-btn-hover-color:#fff;
  --bs-btn-active-bg:#1a1f1c;--bs-btn-active-color:#fff;width:44px;height:44px;padding:0;display:grid;place-items:center}
.essay-bar svg{width:18px;height:18px}
.essay-bar .btn:focus-visible{outline:3px solid var(--essay-accent);outline-offset:3px;box-shadow:none}
@media (max-width:640px){
  .pr-play{bottom:22px}.pr .carousel-caption{bottom:82px}
  .essay .carousel-item img{aspect-ratio:4/3}.essay-cap{padding:22px 22px 92px}.essay-bar{left:22px;right:22px;bottom:26px;justify-content:space-between}}
@media (prefers-reduced-motion:reduce){
  .pr .carousel-indicators .active i::before{animation:none;transform:none}
  .pr-play{display:none}.pr .carousel-caption{bottom:56px}
  .essay-line::before{transition:none}}
JavaScript
// Autoplay without Bootstrap's interval: the active indicator's progress bar is a CSS animation and
// its animationend calls next(). Hover, focus inside the carousel and the pause button pause the
// animation; prefers-reduced-motion removes it, so nothing advances on its own.
document.querySelectorAll('.carousel').forEach((el) => {
  const carousel = bootstrap.Carousel.getOrCreateInstance(el);
  const dots = [...el.querySelectorAll('.carousel-indicators [data-bs-slide-to]')];
  const count = el.querySelector('[data-count]');
  const play = el.querySelector('[data-play]');
  const total = el.querySelectorAll('.carousel-item').length;
  el.addEventListener('slide.bs.carousel', (e) => {
    dots.forEach((dot, n) => dot.toggleAttribute('data-done', n < e.to));
    if (count) count.textContent = String(e.to + 1).padStart(2, '0');
    el.style.setProperty('--progress', (e.to + 1) / total);
  });
  el.addEventListener('animationend', (e) => { if (e.target.closest('.carousel-indicators .active')) carousel.next(); });
  play?.addEventListener('click', () => {
    const paused = el.classList.toggle('is-paused');
    play.setAttribute('aria-label', paused ? 'Play slideshow' : 'Pause slideshow');
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5c615d] uppercase">Autoplay with story-style progress</span>
<section class="pr group/pr relative overflow-hidden rounded-[20px] bg-[#15201b] text-white" role="region" aria-roledescription="carousel" aria-label="Autumn collection" data-carousel data-loop>
  <div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none]" data-track>
    <div class="relative h-[clamp(460px,50vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(100deg,rgba(10,16,13,.85),rgba(10,16,13,.55)_45%,rgba(10,16,13,.05)_80%)]" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=1600&h=900&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" alt="Snow-dusted peaks rising above a sea of cloud at sunset">
      <div class="absolute right-[22px] bottom-[82px] left-[clamp(22px,6vw,64px)] z-[1] max-w-[500px] sm:bottom-[clamp(84px,9vw,96px)] motion-reduce:bottom-14 sm:motion-reduce:bottom-14"><span class="text-[.74rem] font-bold tracking-[.14em] text-pr-accent uppercase">Autumn '26 · Shell layers</span><h2 class="mt-2 mb-[.6rem] text-[clamp(1.7rem,3.6vw,2.7rem)] leading-[1.08] font-bold tracking-[-.025em]">Built for the weather above the weather</h2><p class="leading-[1.55] text-white/90">The Ridgeline Storm shell weighs 310 g, packs into its own pocket and keeps a four-hour downpour out.</p><a class="mt-[1.2rem] inline-flex items-center gap-2 rounded-[10px] bg-pr-accent px-[1.2rem] py-[.7rem] text-[.9rem] font-bold text-[#15201b] transition-[background-color] duration-200 hover:bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent motion-reduce:transition-none" href="#">Shop the range <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="relative h-[clamp(460px,50vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(100deg,rgba(10,16,13,.85),rgba(10,16,13,.55)_45%,rgba(10,16,13,.05)_80%)]" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <img src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?auto=format&fit=crop&w=1600&h=900&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" loading="lazy" alt="A walker on a rock outcrop above a green valley at sunrise">
      <div class="absolute right-[22px] bottom-[82px] left-[clamp(22px,6vw,64px)] z-[1] max-w-[500px] sm:bottom-[clamp(84px,9vw,96px)] motion-reduce:bottom-14 sm:motion-reduce:bottom-14"><span class="text-[.74rem] font-bold tracking-[.14em] text-pr-accent uppercase">Autumn '26 · Insulation</span><h2 class="mt-2 mb-[.6rem] text-[clamp(1.7rem,3.6vw,2.7rem)] leading-[1.08] font-bold tracking-[-.025em]">Warm when you stop, dry when you don't</h2><p class="leading-[1.55] text-white/90">Recycled synthetic fill that keeps its loft wet. The Hollow vest goes under a shell or over a base layer.</p><a class="mt-[1.2rem] inline-flex items-center gap-2 rounded-[10px] bg-pr-accent px-[1.2rem] py-[.7rem] text-[.9rem] font-bold text-[#15201b] transition-[background-color] duration-200 hover:bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent motion-reduce:transition-none" href="#">Shop the range <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="relative h-[clamp(460px,50vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(100deg,rgba(10,16,13,.85),rgba(10,16,13,.55)_45%,rgba(10,16,13,.05)_80%)]" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1600&h=900&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" loading="lazy" alt="Pine forest and a wide glacial valley below snowy mountains">
      <div class="absolute right-[22px] bottom-[82px] left-[clamp(22px,6vw,64px)] z-[1] max-w-[500px] sm:bottom-[clamp(84px,9vw,96px)] motion-reduce:bottom-14 sm:motion-reduce:bottom-14"><span class="text-[.74rem] font-bold tracking-[.14em] text-pr-accent uppercase">Autumn '26 · Footwear</span><h2 class="mt-2 mb-[.6rem] text-[clamp(1.7rem,3.6vw,2.7rem)] leading-[1.08] font-bold tracking-[-.025em]">A boot that is broken in on day one</h2><p class="leading-[1.55] text-white/90">The Cairn mid uses a sock-fit liner and a Vibram sole. Three widths, half sizes from 36 to 48.</p><a class="mt-[1.2rem] inline-flex items-center gap-2 rounded-[10px] bg-pr-accent px-[1.2rem] py-[.7rem] text-[.9rem] font-bold text-[#15201b] transition-[background-color] duration-200 hover:bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent motion-reduce:transition-none" href="#">Shop the range <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
    <div class="relative h-[clamp(460px,50vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(100deg,rgba(10,16,13,.85),rgba(10,16,13,.55)_45%,rgba(10,16,13,.05)_80%)]" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <img src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?auto=format&fit=crop&w=1600&h=900&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" loading="lazy" alt="Sunlight falling across a path through a tall pine forest">
      <div class="absolute right-[22px] bottom-[82px] left-[clamp(22px,6vw,64px)] z-[1] max-w-[500px] sm:bottom-[clamp(84px,9vw,96px)] motion-reduce:bottom-14 sm:motion-reduce:bottom-14"><span class="text-[.74rem] font-bold tracking-[.14em] text-pr-accent uppercase">Autumn '26 · Repairs</span><h2 class="mt-2 mb-[.6rem] text-[clamp(1.7rem,3.6vw,2.7rem)] leading-[1.08] font-bold tracking-[-.025em]">Send it back and we will fix it</h2><p class="leading-[1.55] text-white/90">Free repairs for the life of every Ridgeline jacket. Last year our workshop mended 4,212 of them.</p><a class="mt-[1.2rem] inline-flex items-center gap-2 rounded-[10px] bg-pr-accent px-[1.2rem] py-[.7rem] text-[.9rem] font-bold text-[#15201b] transition-[background-color] duration-200 hover:bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent motion-reduce:transition-none" href="#">Shop the range <svg class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg></a></div>
    </div>
  </div>
  <div class="absolute top-3.5 right-[clamp(22px,6vw,64px)] left-[clamp(22px,6vw,64px)] z-[2] flex gap-1.5" data-controls hidden>
    <button class="group/seg h-6 flex-1 cursor-pointer p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent" type="button" data-dot aria-label="Show slide 1"><i class="pr-fill block h-1 overflow-hidden rounded-xs bg-white/40 before:block before:h-full before:origin-left before:scale-x-0 before:bg-white group-data-done/seg:before:scale-x-100"></i></button>
    <button class="group/seg h-6 flex-1 cursor-pointer p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent" type="button" data-dot aria-label="Show slide 2"><i class="pr-fill block h-1 overflow-hidden rounded-xs bg-white/40 before:block before:h-full before:origin-left before:scale-x-0 before:bg-white group-data-done/seg:before:scale-x-100"></i></button>
    <button class="group/seg h-6 flex-1 cursor-pointer p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent" type="button" data-dot aria-label="Show slide 3"><i class="pr-fill block h-1 overflow-hidden rounded-xs bg-white/40 before:block before:h-full before:origin-left before:scale-x-0 before:bg-white group-data-done/seg:before:scale-x-100"></i></button>
    <button class="group/seg h-6 flex-1 cursor-pointer p-0 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent" type="button" data-dot aria-label="Show slide 4"><i class="pr-fill block h-1 overflow-hidden rounded-xs bg-white/40 before:block before:h-full before:origin-left before:scale-x-0 before:bg-white group-data-done/seg:before:scale-x-100"></i></button>
  </div>
  <button class="absolute bottom-[22px] left-[clamp(22px,6vw,64px)] z-[2] grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-white/70 bg-[rgba(10,16,13,.35)] text-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-pr-accent sm:bottom-[26px] motion-reduce:hidden" type="button" data-play data-controls hidden aria-label="Pause slideshow"><svg class="size-[18px] group-[.is-paused]/pr:hidden" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg><svg class="hidden size-[18px] group-[.is-paused]/pr:block" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13a1 1 0 0 0 1.5.9l10.4-6.5a1 1 0 0 0 0-1.8L9.5 4.6A1 1 0 0 0 8 5.5z"/></svg></button>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5c615d] uppercase">Manual slides with a counter and progress line</span>
<section class="relative overflow-hidden rounded-[20px] bg-white shadow-[0_18px_40px_rgba(26,31,28,.08)]" role="region" aria-roledescription="carousel" aria-label="Places we walked this year" data-carousel data-loop>
  <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-3 focus-visible:outline-essay-accent" data-track tabindex="0">
    <div class="shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <img src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=1600&h=760&q=70" class="block aspect-[4/3] w-full object-cover sm:aspect-[16/7]" width="1600" height="760" alt="Green cliffs and a winding road under a cloudy sunrise">
      <div class="px-[22px] pt-[22px] pb-[92px] sm:pt-[26px] sm:pr-[250px] sm:pb-[30px] sm:pl-8"><h3 class="text-[1.3rem] font-bold tracking-[-.015em]">Quiraing, Isle of Skye</h3><p class="mt-2 max-w-[60ch] leading-[1.6] text-[#4a524d]">We started at 5 am to have the ridge to ourselves. The cloud lifted off the Trotternish cliffs just as the sun cleared the sea.</p></div>
    </div>
    <div class="shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <img src="https://images.unsplash.com/photo-1511884642898-4c92249e20b6?auto=format&fit=crop&w=1600&h=760&q=70" class="block aspect-[4/3] w-full object-cover sm:aspect-[16/7]" width="1600" height="760" loading="lazy" alt="A river running through a misty pine forest in a steep valley">
      <div class="px-[22px] pt-[22px] pb-[92px] sm:pt-[26px] sm:pr-[250px] sm:pb-[30px] sm:pl-8"><h3 class="text-[1.3rem] font-bold tracking-[-.015em]">Glen Etive, Highlands</h3><p class="mt-2 max-w-[60ch] leading-[1.6] text-[#4a524d]">Two days of rain turned the burn into a torrent. We waited it out under the pines and crossed on the third morning.</p></div>
    </div>
    <div class="shrink-0 basis-full snap-start snap-always" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1600&h=760&q=70" class="block aspect-[4/3] w-full object-cover sm:aspect-[16/7]" width="1600" height="760" loading="lazy" alt="A road running between red sandstone cliffs">
      <div class="px-[22px] pt-[22px] pb-[92px] sm:pt-[26px] sm:pr-[250px] sm:pb-[30px] sm:pl-8"><h3 class="text-[1.3rem] font-bold tracking-[-.015em]">Valley of Fire, Nevada</h3><p class="mt-2 max-w-[60ch] leading-[1.6] text-[#4a524d]">Twenty miles of red sandstone and not a single tree. We walked at dawn and dusk and slept through the heat.</p></div>
    </div>
  </div>
  <div class="absolute inset-x-0 bottom-0 h-1 bg-[#e7e5e0] before:absolute before:inset-0 before:origin-left before:scale-x-(--progress,.333) before:bg-essay-accent before:transition-transform before:duration-400 motion-reduce:before:transition-none"></div>
  <div class="absolute right-[22px] bottom-[26px] left-[22px] flex items-center justify-between gap-[18px] sm:right-8 sm:bottom-[34px] sm:left-auto sm:justify-start" data-controls hidden>
    <p class="text-[.95rem] font-semibold text-[#4a524d] tabular-nums" aria-hidden="true"><span class="text-[1.6rem] font-extrabold text-[#1a1f1c]" data-count>01</span> / 03</p>
    <div class="flex gap-2"><button class="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#1a1f1c] text-[#1a1f1c] transition-[background-color,color] duration-200 hover:bg-[#1a1f1c] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-essay-accent motion-reduce:transition-none" type="button" data-prev aria-label="Previous place"><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="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#1a1f1c] text-[#1a1f1c] transition-[background-color,color] duration-200 hover:bg-[#1a1f1c] hover:text-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-essay-accent motion-reduce:transition-none" type="button" data-next aria-label="Next place"><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>
</section></div>
CSS
@theme {
  --color-pr-accent: #fbbf24;
  --color-essay-accent: #c2410c;
}
/* The active segment fills over the interval; pausing this animation pauses the autoplay */
@keyframes pr-fill { to { scale: 1; } }
[aria-current="true"] > .pr-fill::before { animation: pr-fill var(--pr-interval, 6s) linear forwards; }
:is(.pr:hover, .pr:focus-within, .pr.is-paused) .pr-fill::before { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) { [aria-current="true"] > .pr-fill::before { animation: none; scale: 1; } }
JavaScript
// Scroll-snap carousel with an optional autoplay. The track scrolls and swipes without this script.
// Autoplay: the active progress bar is a CSS animation, and when it ends the carousel moves on.
// Hover, keyboard focus inside the carousel and the pause button pause that animation, and
// prefers-reduced-motion removes it, so under reduced motion nothing advances on its own.
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 count = root.querySelector('[data-count]');
  const play = root.querySelector('[data-play]');
  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);
    go(current() + dir);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => { dot.setAttribute('aria-current', n === i); dot.toggleAttribute('data-done', n < i); });
    if (count) count.textContent = String(i + 1).padStart(2, '0');
    root.style.setProperty('--progress', (i + 1) / slides.length);
  };
  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.addEventListener('animationend', (e) => {
    if (e.target.closest('[data-dot]')?.getAttribute('aria-current') === 'true') move(1);
  });
  play?.addEventListener('click', () => {
    const paused = root.classList.toggle('is-paused');
    play.setAttribute('aria-label', paused ? 'Play slideshow' : 'Pause slideshow');
  });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

About this set

Full-width slides that show how far along you are. Autoplay with story-style progress is an outdoor clothing campaign with four slides and a row of segments across the top, like social media stories: the current segment fills over six seconds, then the next slide comes in. A pause button sits in the corner, and the slideshow also pauses while the pointer is over it or keyboard focus is inside it. Manual slides with a counter and progress line is a photo essay that only moves when asked, with a large 01 / 03 counter, arrow buttons and a thin orange line along the bottom that grows as you go. The autoplay needs no timer: the filling segment is a CSS animation, and the script moves on when that animation ends. Pausing the animation therefore pauses the carousel, and removing it under prefers-reduced-motion switches autoplay off completely. The plain CSS and Tailwind versions sit on a scroll-snap track; the Bootstrap version uses .carousel and calls next() from the same animationend event instead of Bootstrap’s own interval.

What’s included

  • Story-style segments that fill while each slide is showing
  • Pause and play button, plus pause on hover and on keyboard focus
  • No autoplay under prefers-reduced-motion
  • Slide counter and a progress line for manual slides
  • Timing lives in one CSS variable, --pr-interval

Accessibility

Both carousels are labelled regions with slides announced as "1 of 4". The autoplay has a visible button that changes its name between "Pause slideshow" and "Play slideshow", and it stops while keyboard focus is inside, which meets WCAG 2.2.2. The segments are buttons named "Show slide 2" with aria-current. Under prefers-reduced-motion nothing advances on its own, the pause button is hidden, and the current segment simply shows as full.

Customise it

Set --pr-interval for the time per slide and --pr-accent for the caption colour on .pr, and --essay-accent for the progress line on .essay. In Tailwind the colours are --color-pr-accent and --color-essay-accent in @theme, and --pr-interval can be set on the section.