Open demo

CSS code

HTML
<div><span class="lbl">Full-bleed hero with captions</span>
<section class="hero" role="region" aria-roledescription="carousel" aria-label="Featured trips" data-carousel data-loop>
  <div class="hero-track" data-track>
    <div class="hero-slide" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <img src="https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=1600&q=70" width="1600" height="900" alt="Moraine Lake below snow-capped peaks at sunrise">
      <div class="hero-cap"><span class="hero-kicker">Canadian Rockies · 9 days</span><h2>Glacier lakes at first light</h2>
        <p>Paddle Moraine Lake before the tour buses arrive and sleep in two backcountry lodges on the way to Lake O'Hara.</p>
        <a class="hero-btn" href="#">View itinerary <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="hero-slide" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <img src="https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&w=1600&q=70" width="1600" height="900" loading="lazy" alt="A wooden rowing boat on the turquoise water of Lago di Braies">
      <div class="hero-cap"><span class="hero-kicker">Dolomites · 7 days</span><h2>Hut to hut across the Dolomites</h2>
        <p>Six nights in family-run rifugi, with luggage moved ahead of you and a rest day at Lago di Braies.</p>
        <a class="hero-btn" href="#">View itinerary <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="hero-slide" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <img src="https://images.unsplash.com/photo-1454496522488-7a8e488e8606?auto=format&fit=crop&w=1600&q=70" width="1600" height="900" loading="lazy" alt="Snow-covered Himalayan summit under a clear sky">
      <div class="hero-cap"><span class="hero-kicker">Nepal · 14 days</span><h2>The slow road to Base Camp</h2>
        <p>An extra acclimatisation day in Namche and Dingboche, and a Sherpa guide who has walked it forty times.</p>
        <a class="hero-btn" href="#">View itinerary <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="hero-slide" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <img src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1600&q=70" width="1600" height="900" loading="lazy" alt="Waves running up an empty sandy beach at sunset">
      <div class="hero-cap"><span class="hero-kicker">Algarve · 6 days</span><h2>Cliff paths and empty coves</h2>
        <p>Walk the Seven Hanging Valleys trail in spring, with a boat back along the cliffs on the last afternoon.</p>
        <a class="hero-btn" href="#">View itinerary <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="hero-arrow" type="button" data-prev data-controls hidden aria-label="Previous slide"><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="hero-arrow" type="button" data-next data-controls hidden aria-label="Next slide"><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 class="hero-dots" data-controls hidden>
    <button type="button" data-dot aria-label="Show slide 1"></button>
    <button type="button" data-dot aria-label="Show slide 2"></button>
    <button type="button" data-dot aria-label="Show slide 3"></button>
    <button type="button" data-dot aria-label="Show slide 4"></button>
  </div>
</section></div>

<div><span class="lbl">Split panel with counter</span>
<section class="split" role="region" aria-roledescription="carousel" aria-label="Recent projects" data-carousel data-loop>
  <div class="split-track" data-track>
    <div class="split-slide" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=1100&q=70" width="1100" height="800" alt="Living room with a mustard armchair, a brass floor lamp and a framed print">
      <div class="split-body"><p class="split-count" aria-hidden="true">01 <span>/ 03</span></p>
        <p class="split-kicker">Residential · Lisbon · 2025</p><h3>Harbour Loft</h3>
        <p>A former sail store turned into a two-bedroom flat. We kept the concrete floor, added oak joinery and let one yellow chair carry the colour.</p>
        <a class="split-link" href="#">Read the case study <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="split-slide" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <img src="https://images.unsplash.com/photo-1524758631624-e2822e304c36?auto=format&fit=crop&w=1100&q=70" width="1100" height="800" loading="lazy" alt="Bright open-plan office lounge with grey sofas and black floor lamps">
      <div class="split-body"><p class="split-count" aria-hidden="true">02 <span>/ 03</span></p>
        <p class="split-kicker">Workplace · Porto · 2024</p><h3>Fieldwork Studio</h3>
        <p>An office for forty people with no assigned desks. Glass walls keep the daylight moving and the lounge doubles as the Friday talk space.</p>
        <a class="split-link" href="#">Read the case study <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="split-slide" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <img src="https://images.unsplash.com/photo-1519710164239-da123dc03ef4?auto=format&fit=crop&w=1100&q=70" width="1100" height="800" loading="lazy" alt="Round white dining table with rattan chairs by a sheer curtain">
      <div class="split-body"><p class="split-count" aria-hidden="true">03 <span>/ 03</span></p>
        <p class="split-kicker">Residential · Sintra · 2023</p><h3>Casa Alba</h3>
        <p>A weekend house painted in six shades of chalk white, with linen, rattan and a dining corner that catches the evening sun.</p>
        <a class="split-link" href="#">Read the case study <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="split-nav" data-controls hidden>
    <button type="button" data-prev aria-label="Previous project"><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 project"><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
/* Hero carousels: a CSS scroll-snap track that swipes and scrolls on its own.
   The script only reveals the arrows and dots and keeps them in sync. */
[data-controls][hidden]{display:none}
.hero{--hero-h:clamp(440px,52vw,560px);--hero-radius:22px;--hero-accent:#fcd34d;
  position:relative;border-radius:var(--hero-radius);overflow:hidden;background:#1f1a14;color:#fff}
.hero-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.hero.is-ready .hero-track{scrollbar-width:none}
.hero-slide{position:relative;flex:0 0 100%;height:var(--hero-h);scroll-snap-align:start;scroll-snap-stop:always}
.hero-slide img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.hero-slide::after{content:"";position:absolute;inset:0;
  background:linear-gradient(to top,rgba(20,15,10,.88),rgba(20,15,10,.55) 50%,rgba(20,15,10,.1) 85%)}
.hero-cap{position:absolute;z-index:1;left:clamp(22px,8vw,96px);right:clamp(22px,8vw,96px);bottom:clamp(76px,8vw,84px);max-width:540px}
.hero-kicker{font-size:.74rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--hero-accent)}
.hero-cap h2{font:600 clamp(1.75rem,3.8vw,2.9rem)/1.06 Georgia,"Times New Roman",serif;letter-spacing:-.01em;margin:.45rem 0 .6rem}
.hero-cap p{font-size:1rem;line-height:1.55;color:rgba(255,255,255,.9);max-width:46ch}
.hero-btn{display:inline-flex;align-items:center;gap:.5rem;margin-top:1.15rem;padding:.72rem 1.25rem;border-radius:999px;
  background:#fff;color:#1f1a14;font-size:.9rem;font-weight:600;text-decoration:none;transition:background .2s}
.hero-btn:hover{background:var(--hero-accent)}
.hero-btn svg{width:16px;height:16px}
.hero-arrow{position:absolute;z-index:2;top:50%;width:48px;height:48px;margin-top:-24px;display:grid;place-items:center;
  border:0;border-radius:50%;background:rgba(255,255,255,.94);color:#1f1a14;cursor:pointer;
  box-shadow:0 8px 22px rgba(0,0,0,.2);transition:transform .2s,background .2s}
.hero-arrow:hover{background:#fff;transform:scale(1.06)}
.hero-arrow[data-prev]{left:18px}.hero-arrow[data-next]{right:18px}
.hero-arrow svg{width:20px;height:20px}
.hero-dots{position:absolute;z-index:2;left:clamp(22px,8vw,96px);bottom:26px;display:flex;gap:4px}
.hero-dots button{width:24px;height:24px;padding:0;border:0;background:none;cursor:pointer;transition:width .25s}
.hero-dots button::before{content:"";display:block;height:6px;border-radius:3px;background:rgba(255,255,255,.6);transition:background .25s}
.hero-dots button[aria-current=true]{width:40px}
.hero-dots button[aria-current=true]::before{background:#fff}
.hero-btn:focus-visible,.hero-arrow:focus-visible,.hero-dots button:focus-visible{outline:3px solid var(--hero-accent);outline-offset:3px}
@media (max-width:640px){
  .hero-arrow{top:auto;bottom:18px;margin:0;width:42px;height:42px}
  .hero-arrow[data-prev]{left:auto;right:68px}.hero-arrow[data-next]{right:18px}
  .hero-dots{bottom:27px}}

/* Split: image on one side, the caption on a panel beside it */
.split{--split-bg:#e9dfd1;--split-ink:#2a2119;--split-accent:#9a3f1c;
  position:relative;border-radius:22px;overflow:hidden;background:var(--split-bg);color:var(--split-ink)}
.split-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.split.is-ready .split-track{scrollbar-width:none}
.split-slide{flex:0 0 100%;display:grid;grid-template-columns:1.35fr 1fr;scroll-snap-align:start;scroll-snap-stop:always}
.split-slide img{width:100%;height:460px;object-fit:cover;display:block}
.split-body{padding:44px 40px 110px;display:flex;flex-direction:column;justify-content:center}
.split-body .split-count{font:600 2.4rem/1 Georgia,"Times New Roman",serif;color:var(--split-accent)}
.split-body .split-count span{font-size:1rem;color:var(--split-ink)}
.split-body .split-kicker{margin-top:26px;font-size:.74rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#5f4f40}
.split-body h3{font:600 clamp(1.6rem,2.6vw,2.1rem)/1.1 Georgia,"Times New Roman",serif;margin:.4rem 0 .7rem}
.split-body p{font-size:.97rem;line-height:1.6}
.split-link{align-self:flex-start;display:inline-flex;align-items:center;gap:.45rem;margin-top:1.1rem;color:var(--split-accent);
  font-weight:600;font-size:.92rem;text-decoration:underline;text-underline-offset:4px}
.split-link svg{width:16px;height:16px}
.split-nav{position:absolute;right:40px;bottom:36px;display:flex;gap:10px}
.split-nav button{width:46px;height:46px;display:grid;place-items:center;border:1.5px solid var(--split-ink);border-radius:50%;
  background:transparent;color:var(--split-ink);cursor:pointer;transition:background .2s,color .2s}
.split-nav button:hover{background:var(--split-ink);color:var(--split-bg)}
.split-nav svg{width:18px;height:18px}
.split-link:focus-visible,.split-nav button:focus-visible{outline:3px solid var(--split-accent);outline-offset:3px}
@media (max-width:767px){
  .split-slide{grid-template-columns:1fr}
  .split-slide img{height:240px}
  .split-body{padding:26px 24px 92px}
  .split-body .split-count{font-size:1.9rem}.split-body .split-kicker{margin-top:16px}
  .split-nav{right:24px;bottom:24px}}
@media (prefers-reduced-motion:reduce){
  .hero-btn,.hero-arrow,.hero-dots button,.hero-dots button::before,.split-nav button{transition:none}
  .hero-arrow:hover{transform:none}}
JavaScript
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
  const track = root.querySelector('[data-track]');
  const slides = [...track.children];
  const dots = [...root.querySelectorAll('[data-dot]')];
  const prev = root.querySelector('[data-prev]');
  const next = root.querySelector('[data-next]');
  const loop = root.hasAttribute('data-loop');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
  const current = () => Math.round(track.scrollLeft / step());
  const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
  const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
  const move = (dir) => {
    if (loop && dir > 0 && atEnd()) return go(0);
    if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
    const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
    go(current() + dir * perView);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
    if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
    if (!loop && next) next.setAttribute('aria-disabled', atEnd());
  };
  prev?.addEventListener('click', () => move(-1));
  next?.addEventListener('click', () => move(1));
  dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
  track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div><span class="lbl">Full-bleed hero with captions</span>
<div id="heroTrips" class="carousel slide hero" role="region" aria-roledescription="carousel" aria-label="Featured trips">
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#heroTrips" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show slide 1"></button>
    <button type="button" data-bs-target="#heroTrips" data-bs-slide-to="1" aria-label="Show slide 2"></button>
    <button type="button" data-bs-target="#heroTrips" data-bs-slide-to="2" aria-label="Show slide 3"></button>
    <button type="button" data-bs-target="#heroTrips" data-bs-slide-to="3" aria-label="Show slide 4"></button>
  </div>
  <div class="carousel-inner" aria-live="polite">
    <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <img src="https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=1600&q=70" class="d-block w-100" width="1600" height="900" alt="Moraine Lake below snow-capped peaks at sunrise">
      <div class="carousel-caption"><span class="hero-kicker">Canadian Rockies · 9 days</span><h2>Glacier lakes at first light</h2>
        <p>Paddle Moraine Lake before the tour buses arrive and sleep in two backcountry lodges on the way to Lake O'Hara.</p>
        <a class="btn rounded-pill" href="#">View itinerary <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-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&w=1600&q=70" class="d-block w-100" width="1600" height="900" alt="A wooden rowing boat on the turquoise water of Lago di Braies">
      <div class="carousel-caption"><span class="hero-kicker">Dolomites · 7 days</span><h2>Hut to hut across the Dolomites</h2>
        <p>Six nights in family-run rifugi, with luggage moved ahead of you and a rest day at Lago di Braies.</p>
        <a class="btn rounded-pill" href="#">View itinerary <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-1454496522488-7a8e488e8606?auto=format&fit=crop&w=1600&q=70" class="d-block w-100" width="1600" height="900" alt="Snow-covered Himalayan summit under a clear sky">
      <div class="carousel-caption"><span class="hero-kicker">Nepal · 14 days</span><h2>The slow road to Base Camp</h2>
        <p>An extra acclimatisation day in Namche and Dingboche, and a Sherpa guide who has walked it forty times.</p>
        <a class="btn rounded-pill" href="#">View itinerary <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-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1600&q=70" class="d-block w-100" width="1600" height="900" alt="Waves running up an empty sandy beach at sunset">
      <div class="carousel-caption"><span class="hero-kicker">Algarve · 6 days</span><h2>Cliff paths and empty coves</h2>
        <p>Walk the Seven Hanging Valleys trail in spring, with a boat back along the cliffs on the last afternoon.</p>
        <a class="btn rounded-pill" href="#">View itinerary <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="carousel-control-prev" type="button" data-bs-target="#heroTrips" data-bs-slide="prev"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous slide</span></button>
  <button class="carousel-control-next" type="button" data-bs-target="#heroTrips" data-bs-slide="next"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next slide</span></button>
</div></div>

<div><span class="lbl">Split panel with counter</span>
<div id="projects" class="carousel slide split" role="region" aria-roledescription="carousel" aria-label="Recent projects">
  <div class="carousel-inner" aria-live="polite">
    <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <div class="row g-0">
        <div class="col-md-7"><img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=1100&q=70" class="d-block w-100" width="1100" height="800" alt="Living room with a mustard armchair, a brass floor lamp and a framed print"></div>
        <div class="col-md-5 split-body d-flex flex-column justify-content-center"><p class="split-count" aria-hidden="true">01 <span>/ 03</span></p>
          <p class="split-kicker">Residential · Lisbon · 2025</p><h3>Harbour Loft</h3>
          <p class="mb-0">A former sail store turned into a two-bedroom flat. We kept the concrete floor, added oak joinery and let one yellow chair carry the colour.</p>
          <a class="split-link" href="#">Read the case study <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="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <div class="row g-0">
        <div class="col-md-7"><img src="https://images.unsplash.com/photo-1524758631624-e2822e304c36?auto=format&fit=crop&w=1100&q=70" class="d-block w-100" width="1100" height="800" alt="Bright open-plan office lounge with grey sofas and black floor lamps"></div>
        <div class="col-md-5 split-body d-flex flex-column justify-content-center"><p class="split-count" aria-hidden="true">02 <span>/ 03</span></p>
          <p class="split-kicker">Workplace · Porto · 2024</p><h3>Fieldwork Studio</h3>
          <p class="mb-0">An office for forty people with no assigned desks. Glass walls keep the daylight moving and the lounge doubles as the Friday talk space.</p>
          <a class="split-link" href="#">Read the case study <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="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <div class="row g-0">
        <div class="col-md-7"><img src="https://images.unsplash.com/photo-1519710164239-da123dc03ef4?auto=format&fit=crop&w=1100&q=70" class="d-block w-100" width="1100" height="800" alt="Round white dining table with rattan chairs by a sheer curtain"></div>
        <div class="col-md-5 split-body d-flex flex-column justify-content-center"><p class="split-count" aria-hidden="true">03 <span>/ 03</span></p>
          <p class="split-kicker">Residential · Sintra · 2023</p><h3>Casa Alba</h3>
          <p class="mb-0">A weekend house painted in six shades of chalk white, with linen, rattan and a dining corner that catches the evening sun.</p>
          <a class="split-link" href="#">Read the case study <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>
  <div class="split-nav d-flex gap-2">
    <button class="btn rounded-circle" type="button" data-bs-target="#projects" data-bs-slide="prev" aria-label="Previous project"><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="#projects" data-bs-slide="next" aria-label="Next project"><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>
CSS
/* Hero carousels on Bootstrap's own .carousel: indicators, controls and captions restyled. */
.hero{--hero-h:clamp(440px,52vw,560px);--hero-accent:#fcd34d;
  border-radius:22px;overflow:hidden;background:#1f1a14;color:#fff}
.hero .carousel-item img{height:var(--hero-h);object-fit:cover}
.hero .carousel-item::after{content:"";position:absolute;inset:0;
  background:linear-gradient(to top,rgba(20,15,10,.88),rgba(20,15,10,.55) 50%,rgba(20,15,10,.1) 85%)}
.hero .carousel-caption{z-index:1;left:clamp(22px,8vw,96px);right:clamp(22px,8vw,96px);bottom:clamp(76px,8vw,84px);
  max-width:540px;padding:0;text-align:left;color:#fff}
.hero-kicker{font-size:.74rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--hero-accent)}
.hero .carousel-caption h2{font:600 clamp(1.75rem,3.8vw,2.9rem)/1.06 Georgia,"Times New Roman",serif;letter-spacing:-.01em;margin:.45rem 0 .6rem}
.hero .carousel-caption p{font-size:1rem;line-height:1.55;color:rgba(255,255,255,.9);max-width:46ch;margin:0}
.hero .btn{--bs-btn-padding-x:1.25rem;--bs-btn-padding-y:.72rem;--bs-btn-font-size:.9rem;--bs-btn-font-weight:600;--bs-btn-line-height:normal;--bs-btn-border-width:0;
  --bs-btn-bg:#fff;--bs-btn-color:#1f1a14;--bs-btn-border-color:#fff;--bs-btn-hover-bg:var(--hero-accent);--bs-btn-hover-border-color:var(--hero-accent);
  --bs-btn-hover-color:#1f1a14;--bs-btn-active-bg:var(--hero-accent);--bs-btn-active-color:#1f1a14;
  display:inline-flex;align-items:center;gap:.5rem;margin-top:1.15rem}
.hero .btn svg{width:16px;height:16px}
.hero .carousel-control-prev,.hero .carousel-control-next{top:50%;bottom:auto;width:48px;height:48px;margin-top:-24px;
  border-radius:50%;background:rgba(255,255,255,.94);opacity:1;box-shadow:0 8px 22px rgba(0,0,0,.2);transition:transform .2s,background .2s}
.hero .carousel-control-prev{left:18px}.hero .carousel-control-next{right:18px}
.hero .carousel-control-prev:hover,.hero .carousel-control-next:hover{background:#fff;transform:scale(1.06)}
.hero .carousel-control-prev-icon,.hero .carousel-control-next-icon{width:18px;height:18px;filter:invert(1)}
.hero .carousel-indicators{left:clamp(22px,8vw,96px);right:auto;bottom:26px;margin:0;gap:4px;justify-content:flex-start}
.hero .carousel-indicators [data-bs-target]{width:24px;height:24px;margin:0;border:0;background:none;opacity:1;text-indent:0;transition:width .25s}
.hero .carousel-indicators [data-bs-target]::before{content:"";display:block;height:6px;border-radius:3px;background:rgba(255,255,255,.6);transition:background .25s}
.hero .carousel-indicators .active{width:40px}
.hero .carousel-indicators .active::before{background:#fff}
.hero .btn:focus-visible,.hero .carousel-control-prev:focus-visible,.hero .carousel-control-next:focus-visible,
.hero .carousel-indicators [data-bs-target]:focus-visible{outline:3px solid var(--hero-accent);outline-offset:3px;box-shadow:none}
@media (max-width:640px){
  .hero .carousel-control-prev,.hero .carousel-control-next{top:auto;bottom:18px;margin:0;width:42px;height:42px}
  .hero .carousel-control-prev{left:auto;right:68px}.hero .carousel-control-next{right:18px}
  .hero .carousel-indicators{bottom:27px}}

/* Split: image on one side, the caption on a panel beside it */
.split{--split-bg:#e9dfd1;--split-ink:#2a2119;--split-accent:#9a3f1c;border-radius:22px;overflow:hidden;background:var(--split-bg);color:var(--split-ink)}
.split img{height:460px;object-fit:cover}
.split-body{padding:44px 40px 110px}
.split-body .split-count{font:600 2.4rem/1 Georgia,"Times New Roman",serif;color:var(--split-accent);margin:0}
.split-body .split-count span{font-size:1rem;color:var(--split-ink)}
.split-body .split-kicker{margin:26px 0 0;font-size:.74rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#5f4f40}
.split-body h3{font:600 clamp(1.6rem,2.6vw,2.1rem)/1.1 Georgia,"Times New Roman",serif;margin:.4rem 0 .7rem}
.split-body p{font-size:.97rem;line-height:1.6}
.split-link{align-self:flex-start;display:inline-flex;align-items:center;gap:.45rem;margin-top:1.1rem;color:var(--split-accent);
  font-weight:600;font-size:.92rem;text-underline-offset:4px}
.split-link svg{width:16px;height:16px}
.split-nav{position:absolute;z-index:2;right:40px;bottom:36px}
.split-nav .btn{--bs-btn-color:var(--split-ink);--bs-btn-border-color:var(--split-ink);--bs-btn-hover-bg:var(--split-ink);--bs-btn-hover-border-color:var(--split-ink);
  --bs-btn-hover-color:var(--split-bg);--bs-btn-active-bg:var(--split-ink);--bs-btn-active-color:var(--split-bg);--bs-btn-border-width:1.5px;
  width:46px;height:46px;padding:0;display:grid;place-items:center}
.split-nav svg{width:18px;height:18px}
.split-link:focus-visible,.split-nav .btn:focus-visible{outline:3px solid var(--split-accent);outline-offset:3px;box-shadow:none}
@media (max-width:767px){
  .split img{height:240px}
  .split-body{padding:26px 24px 92px}
  .split-body .split-count{font-size:1.9rem}.split-body .split-kicker{margin-top:16px}
  .split-nav{right:24px;bottom:24px}}
@media (prefers-reduced-motion:reduce){
  .hero .carousel-control-prev,.hero .carousel-control-next,.hero .carousel-indicators [data-bs-target],
  .hero .carousel-indicators [data-bs-target]::before{transition:none}
  .hero .carousel-control-prev:hover,.hero .carousel-control-next:hover{transform:none}}
JavaScript
// Create the instances up front so the arrow keys work before the first click. No autoplay (ride is off).
document.querySelectorAll('.carousel').forEach((el) => bootstrap.Carousel.getOrCreateInstance(el));

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6258] uppercase">Full-bleed hero with captions</span>
<section class="relative overflow-hidden rounded-[22px] bg-[#1f1a14] text-white" role="region" aria-roledescription="carousel" aria-label="Featured trips" 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(440px,52vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,15,10,.88),rgba(20,15,10,.55)_50%,rgba(20,15,10,.1)_85%)]" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <img src="https://images.unsplash.com/photo-1493246507139-91e8fad9978e?auto=format&fit=crop&w=1600&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" alt="Moraine Lake below snow-capped peaks at sunrise">
      <div class="absolute right-[clamp(22px,8vw,96px)] bottom-[clamp(76px,8vw,84px)] left-[clamp(22px,8vw,96px)] z-[1] max-w-[540px]"><span class="text-[.74rem] font-bold tracking-[.14em] text-hero-accent uppercase">Canadian Rockies · 9 days</span><h2 class="mt-[.45rem] mb-[.6rem] font-display text-[clamp(1.75rem,3.8vw,2.9rem)] leading-[1.06] font-semibold tracking-[-.01em]">Glacier lakes at first light</h2>
        <p class="max-w-[46ch] leading-[1.55] text-white/90">Paddle Moraine Lake before the tour buses arrive and sleep in two backcountry lodges on the way to Lake O'Hara.</p>
        <a class="mt-[1.15rem] inline-flex items-center gap-2 rounded-full bg-white px-5 py-[.72rem] text-[.9rem] font-semibold text-[#1f1a14] transition-[background-color] duration-200 hover:bg-hero-accent focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent motion-reduce:transition-none" href="#">View itinerary <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(440px,52vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,15,10,.88),rgba(20,15,10,.55)_50%,rgba(20,15,10,.1)_85%)]" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <img src="https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&w=1600&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" loading="lazy" alt="A wooden rowing boat on the turquoise water of Lago di Braies">
      <div class="absolute right-[clamp(22px,8vw,96px)] bottom-[clamp(76px,8vw,84px)] left-[clamp(22px,8vw,96px)] z-[1] max-w-[540px]"><span class="text-[.74rem] font-bold tracking-[.14em] text-hero-accent uppercase">Dolomites · 7 days</span><h2 class="mt-[.45rem] mb-[.6rem] font-display text-[clamp(1.75rem,3.8vw,2.9rem)] leading-[1.06] font-semibold tracking-[-.01em]">Hut to hut across the Dolomites</h2>
        <p class="max-w-[46ch] leading-[1.55] text-white/90">Six nights in family-run rifugi, with luggage moved ahead of you and a rest day at Lago di Braies.</p>
        <a class="mt-[1.15rem] inline-flex items-center gap-2 rounded-full bg-white px-5 py-[.72rem] text-[.9rem] font-semibold text-[#1f1a14] transition-[background-color] duration-200 hover:bg-hero-accent focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent motion-reduce:transition-none" href="#">View itinerary <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(440px,52vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,15,10,.88),rgba(20,15,10,.55)_50%,rgba(20,15,10,.1)_85%)]" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <img src="https://images.unsplash.com/photo-1454496522488-7a8e488e8606?auto=format&fit=crop&w=1600&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" loading="lazy" alt="Snow-covered Himalayan summit under a clear sky">
      <div class="absolute right-[clamp(22px,8vw,96px)] bottom-[clamp(76px,8vw,84px)] left-[clamp(22px,8vw,96px)] z-[1] max-w-[540px]"><span class="text-[.74rem] font-bold tracking-[.14em] text-hero-accent uppercase">Nepal · 14 days</span><h2 class="mt-[.45rem] mb-[.6rem] font-display text-[clamp(1.75rem,3.8vw,2.9rem)] leading-[1.06] font-semibold tracking-[-.01em]">The slow road to Base Camp</h2>
        <p class="max-w-[46ch] leading-[1.55] text-white/90">An extra acclimatisation day in Namche and Dingboche, and a Sherpa guide who has walked it forty times.</p>
        <a class="mt-[1.15rem] inline-flex items-center gap-2 rounded-full bg-white px-5 py-[.72rem] text-[.9rem] font-semibold text-[#1f1a14] transition-[background-color] duration-200 hover:bg-hero-accent focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent motion-reduce:transition-none" href="#">View itinerary <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(440px,52vw,560px)] shrink-0 basis-full snap-start snap-always after:absolute after:inset-0 after:bg-[linear-gradient(to_top,rgba(20,15,10,.88),rgba(20,15,10,.55)_50%,rgba(20,15,10,.1)_85%)]" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <img src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=1600&q=70" class="absolute inset-0 size-full object-cover" width="1600" height="900" loading="lazy" alt="Waves running up an empty sandy beach at sunset">
      <div class="absolute right-[clamp(22px,8vw,96px)] bottom-[clamp(76px,8vw,84px)] left-[clamp(22px,8vw,96px)] z-[1] max-w-[540px]"><span class="text-[.74rem] font-bold tracking-[.14em] text-hero-accent uppercase">Algarve · 6 days</span><h2 class="mt-[.45rem] mb-[.6rem] font-display text-[clamp(1.75rem,3.8vw,2.9rem)] leading-[1.06] font-semibold tracking-[-.01em]">Cliff paths and empty coves</h2>
        <p class="max-w-[46ch] leading-[1.55] text-white/90">Walk the Seven Hanging Valleys trail in spring, with a boat back along the cliffs on the last afternoon.</p>
        <a class="mt-[1.15rem] inline-flex items-center gap-2 rounded-full bg-white px-5 py-[.72rem] text-[.9rem] font-semibold text-[#1f1a14] transition-[background-color] duration-200 hover:bg-hero-accent focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent motion-reduce:transition-none" href="#">View itinerary <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>
  <button class="absolute top-1/2 left-[18px] z-[2] -mt-6 grid size-12 cursor-pointer place-items-center rounded-full bg-white/94 text-[#1f1a14] shadow-[0_8px_22px_rgba(0,0,0,.2)] transition-[transform,background-color] duration-200 hover:scale-106 hover:bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent motion-reduce:transition-none motion-reduce:hover:scale-100 max-sm:top-auto max-sm:right-[68px] max-sm:bottom-[18px] max-sm:left-auto max-sm:mt-0 max-sm:size-[42px]" type="button" data-prev data-controls hidden aria-label="Previous slide"><svg class="size-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
  <button class="absolute top-1/2 right-[18px] z-[2] -mt-6 grid size-12 cursor-pointer place-items-center rounded-full bg-white/94 text-[#1f1a14] shadow-[0_8px_22px_rgba(0,0,0,.2)] transition-[transform,background-color] duration-200 hover:scale-106 hover:bg-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent motion-reduce:transition-none motion-reduce:hover:scale-100 max-sm:top-auto max-sm:bottom-[18px] max-sm:mt-0 max-sm:size-[42px]" type="button" data-next data-controls hidden aria-label="Next slide"><svg class="size-5" 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 class="absolute bottom-[26px] left-[clamp(22px,8vw,96px)] z-[2] flex gap-1 max-sm:bottom-[27px]" data-controls hidden>
    <button class="h-6 w-6 cursor-pointer p-0 transition-[width] duration-250 before:block before:h-1.5 before:rounded-[3px] before:bg-white/60 before:transition-colors before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent aria-[current=true]:w-10 aria-[current=true]:before:bg-white motion-reduce:transition-none motion-reduce:before:transition-none" type="button" data-dot aria-label="Show slide 1"></button>
    <button class="h-6 w-6 cursor-pointer p-0 transition-[width] duration-250 before:block before:h-1.5 before:rounded-[3px] before:bg-white/60 before:transition-colors before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent aria-[current=true]:w-10 aria-[current=true]:before:bg-white motion-reduce:transition-none motion-reduce:before:transition-none" type="button" data-dot aria-label="Show slide 2"></button>
    <button class="h-6 w-6 cursor-pointer p-0 transition-[width] duration-250 before:block before:h-1.5 before:rounded-[3px] before:bg-white/60 before:transition-colors before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent aria-[current=true]:w-10 aria-[current=true]:before:bg-white motion-reduce:transition-none motion-reduce:before:transition-none" type="button" data-dot aria-label="Show slide 3"></button>
    <button class="h-6 w-6 cursor-pointer p-0 transition-[width] duration-250 before:block before:h-1.5 before:rounded-[3px] before:bg-white/60 before:transition-colors before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-hero-accent aria-[current=true]:w-10 aria-[current=true]:before:bg-white motion-reduce:transition-none motion-reduce:before:transition-none" type="button" data-dot aria-label="Show slide 4"></button>
  </div>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#6b6258] uppercase">Split panel with counter</span>
<section class="relative overflow-hidden rounded-[22px] bg-split-bg text-split-ink" role="region" aria-roledescription="carousel" aria-label="Recent projects" 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="grid shrink-0 basis-full snap-start snap-always md:grid-cols-[1.35fr_1fr]" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <img src="https://images.unsplash.com/photo-1586023492125-27b2c045efd7?auto=format&fit=crop&w=1100&q=70" class="block h-60 w-full object-cover md:h-[460px]" width="1100" height="800" alt="Living room with a mustard armchair, a brass floor lamp and a framed print">
      <div class="flex flex-col justify-center px-6 pt-[26px] pb-[92px] md:px-10 md:pt-11 md:pb-[110px]"><p class="font-display text-[1.9rem] leading-none font-semibold text-split-accent md:text-[2.4rem]" aria-hidden="true">01 <span class="text-base text-split-ink">/ 03</span></p>
        <p class="mt-4 text-[.74rem] font-bold tracking-[.14em] text-[#5f4f40] uppercase md:mt-[26px]">Residential · Lisbon · 2025</p><h3 class="mt-[.4rem] mb-[.7rem] font-display text-[clamp(1.6rem,2.6vw,2.1rem)] leading-[1.1] font-semibold">Harbour Loft</h3>
        <p class="text-[.97rem] leading-[1.6]">A former sail store turned into a two-bedroom flat. We kept the concrete floor, added oak joinery and let one yellow chair carry the colour.</p>
        <a class="mt-[1.1rem] inline-flex items-center gap-[.45rem] self-start text-[.92rem] font-semibold text-split-accent underline underline-offset-4 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-split-accent" href="#">Read the case study <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="grid shrink-0 basis-full snap-start snap-always md:grid-cols-[1.35fr_1fr]" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <img src="https://images.unsplash.com/photo-1524758631624-e2822e304c36?auto=format&fit=crop&w=1100&q=70" class="block h-60 w-full object-cover md:h-[460px]" width="1100" height="800" loading="lazy" alt="Bright open-plan office lounge with grey sofas and black floor lamps">
      <div class="flex flex-col justify-center px-6 pt-[26px] pb-[92px] md:px-10 md:pt-11 md:pb-[110px]"><p class="font-display text-[1.9rem] leading-none font-semibold text-split-accent md:text-[2.4rem]" aria-hidden="true">02 <span class="text-base text-split-ink">/ 03</span></p>
        <p class="mt-4 text-[.74rem] font-bold tracking-[.14em] text-[#5f4f40] uppercase md:mt-[26px]">Workplace · Porto · 2024</p><h3 class="mt-[.4rem] mb-[.7rem] font-display text-[clamp(1.6rem,2.6vw,2.1rem)] leading-[1.1] font-semibold">Fieldwork Studio</h3>
        <p class="text-[.97rem] leading-[1.6]">An office for forty people with no assigned desks. Glass walls keep the daylight moving and the lounge doubles as the Friday talk space.</p>
        <a class="mt-[1.1rem] inline-flex items-center gap-[.45rem] self-start text-[.92rem] font-semibold text-split-accent underline underline-offset-4 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-split-accent" href="#">Read the case study <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="grid shrink-0 basis-full snap-start snap-always md:grid-cols-[1.35fr_1fr]" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <img src="https://images.unsplash.com/photo-1519710164239-da123dc03ef4?auto=format&fit=crop&w=1100&q=70" class="block h-60 w-full object-cover md:h-[460px]" width="1100" height="800" loading="lazy" alt="Round white dining table with rattan chairs by a sheer curtain">
      <div class="flex flex-col justify-center px-6 pt-[26px] pb-[92px] md:px-10 md:pt-11 md:pb-[110px]"><p class="font-display text-[1.9rem] leading-none font-semibold text-split-accent md:text-[2.4rem]" aria-hidden="true">03 <span class="text-base text-split-ink">/ 03</span></p>
        <p class="mt-4 text-[.74rem] font-bold tracking-[.14em] text-[#5f4f40] uppercase md:mt-[26px]">Residential · Sintra · 2023</p><h3 class="mt-[.4rem] mb-[.7rem] font-display text-[clamp(1.6rem,2.6vw,2.1rem)] leading-[1.1] font-semibold">Casa Alba</h3>
        <p class="text-[.97rem] leading-[1.6]">A weekend house painted in six shades of chalk white, with linen, rattan and a dining corner that catches the evening sun.</p>
        <a class="mt-[1.1rem] inline-flex items-center gap-[.45rem] self-start text-[.92rem] font-semibold text-split-accent underline underline-offset-4 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-split-accent" href="#">Read the case study <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 right-6 bottom-6 flex gap-2.5 md:right-10 md:bottom-9" data-controls hidden>
    <button class="grid size-[46px] cursor-pointer place-items-center rounded-full border-[1.5px] border-split-ink text-split-ink transition-[background-color,color] duration-200 hover:bg-split-ink hover:text-split-bg focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-split-accent motion-reduce:transition-none" type="button" data-prev aria-label="Previous project"><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-[46px] cursor-pointer place-items-center rounded-full border-[1.5px] border-split-ink text-split-ink transition-[background-color,color] duration-200 hover:bg-split-ink hover:text-split-bg focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-split-accent motion-reduce:transition-none" type="button" data-next aria-label="Next project"><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>
</section></div>
CSS
@theme {
  --color-hero-accent: #fcd34d;
  --color-split-bg: #e9dfd1;
  --color-split-ink: #2a2119;
  --color-split-accent: #9a3f1c;
  --font-display: Georgia, "Times New Roman", serif;
}
JavaScript
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
  const track = root.querySelector('[data-track]');
  const slides = [...track.children];
  const dots = [...root.querySelectorAll('[data-dot]')];
  const prev = root.querySelector('[data-prev]');
  const next = root.querySelector('[data-next]');
  const loop = root.hasAttribute('data-loop');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
  const current = () => Math.round(track.scrollLeft / step());
  const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
  const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
  const move = (dir) => {
    if (loop && dir > 0 && atEnd()) return go(0);
    if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
    const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
    go(current() + dir * perView);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
    if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
    if (!loop && next) next.setAttribute('aria-disabled', atEnd());
  };
  prev?.addEventListener('click', () => move(-1));
  next?.addEventListener('click', () => move(1));
  dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
  track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

About this set

Two hero carousels for the top of a page. Full-bleed hero with captions shows four travel slides with a photo, a dark gradient behind the caption, a heading, a short paragraph and a button, plus round arrows at the sides and pill-shaped dots. Split panel with counter puts the photo on one side and the caption on a sand-coloured panel on the other, with a large 01 / 03 counter and outlined arrow buttons, and stacks the two on phones. Use the full-bleed hero for a landing page and the split panel for case studies or portfolio work, where the text matters as much as the picture. The plain CSS and Tailwind versions are a CSS scroll-snap track, so the slides swipe and scroll with no script at all. A few lines of vanilla JavaScript reveal the arrows and dots, move one slide per click, go back to the first slide after the last, and keep the current dot marked. The Bootstrap version is the stock .carousel with its indicators, controls and captions restyled, and nothing rotates on its own.

What’s included

  • Two layouts: full-bleed photo with caption and split image and text panel
  • Scroll-snap track that swipes without JavaScript in the CSS and Tailwind versions
  • Bootstrap version uses the native .carousel, indicators and controls
  • No autoplay, and the last slide leads back to the first
  • Arrows move to the bottom corner on phones so they never cover the caption

Accessibility

Each carousel is a region with aria-roledescription="carousel" and a label, and each slide is a group announced as "1 of 4", "2 of 4" and so on. The arrows and dots are buttons with names such as "Next slide" and "Show slide 3", and the current dot carries aria-current. Every control shows a 3px outline on keyboard focus, and under prefers-reduced-motion the slides jump instead of scrolling smoothly and the hover transitions are switched off.

Customise it

Change --hero-accent and --hero-h on .hero for the highlight colour and slide height, and --split-bg, --split-ink and --split-accent on .split. In Tailwind the same colours are --color-hero-accent and the --color-split-* theme values, and the height is the h-[clamp()] utility on each slide.