Open demo

CSS code

HTML
<div><span class="lbl">Product slider, four per view</span>
<section class="shelf" role="region" aria-roledescription="carousel" aria-label="New this week" data-carousel>
  <div class="shelf-head">
    <div><p class="shelf-kicker">New this week</p><h2>Made for slow mornings</h2></div>
    <div class="shelf-actions"><a class="shelf-all" href="#">Shop all 48</a>
      <button class="shelf-btn" type="button" data-prev data-controls hidden aria-label="Previous products"><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="shelf-btn" type="button" data-next data-controls hidden aria-label="Next products"><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 class="shelf-track" data-track>
    <div class="item" role="group" aria-roledescription="slide" aria-label="1 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1459411552884-841db9b3cc2a?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Small cactus in a terracotta pot on a pink background"><span class="item-badge">New</span></div>
      <h3 class="item-name"><a href="#">Saguaro in terracotta</a></h3><p class="item-meta">12 cm pot · saucer included</p><p class="item-price">$18</p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="2 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1485955900006-10f4d324d411?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Succulent in a mint green glazed planter"></div>
      <h3 class="item-name"><a href="#">Haworthia, mint planter</a></h3><p class="item-meta">Glazed stoneware · 14 cm</p><p class="item-price">$22</p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="3 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Plain white stoneware mug on a white table"><span class="item-badge sale">−20%</span></div>
      <h3 class="item-name"><a href="#">Stoneware mug, chalk</a></h3><p class="item-meta">350 ml · dishwasher safe</p><p class="item-price">$12 <del><span class="sr-only">Was </span>$15</del></p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="4 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1503602642458-232111445657?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Pale birch wood stool against a blue wall"></div>
      <h3 class="item-name"><a href="#">Birch counter stool</a></h3><p class="item-meta">Solid birch · 65 cm seat</p><p class="item-price">$96</p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="5 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1544816155-12df9643f363?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="Tan canvas tote bag hanging on a grey wall"></div>
      <h3 class="item-name"><a href="#">Waxed canvas tote</a></h3><p class="item-meta">Tobacco · 18 litres</p><p class="item-price">$38</p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="6 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1602143407151-7111542de6e8?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="Matte sage green insulated water bottle"><span class="item-badge">Back in stock</span></div>
      <h3 class="item-name"><a href="#">Insulated bottle, sage</a></h3><p class="item-meta">500 ml · keeps cold 24 h</p><p class="item-price">$26</p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="7 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1517705008128-361805f42e86?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="White moulded chair with wooden legs next to a plant stand"></div>
      <h3 class="item-name"><a href="#">Moulded side chair</a></h3><p class="item-meta">Recycled shell · oak legs</p><p class="item-price">$120</p>
    </div>
    <div class="item" role="group" aria-roledescription="slide" aria-label="8 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="Bottle green velvet three-seater sofa on wooden legs"></div>
      <h3 class="item-name"><a href="#">Velvet sofa, bottle green</a></h3><p class="item-meta">Three-seater · 214 cm</p><p class="item-price">$1,240</p>
    </div>
  </div>
</section></div>

<div><span class="lbl">Editorial cards with a peek</span>
<section class="journal" role="region" aria-roledescription="carousel" aria-label="From the journal" data-carousel>
  <h2>From the journal</h2>
  <div class="journal-rail">
  <div class="journal-track" data-track>
    <div class="story" role="group" aria-roledescription="slide" aria-label="1 of 5">
      <img src="https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?auto=format&fit=crop&w=700&h=460&q=70" width="700" height="460" alt="Sunny living room full of hanging and potted plants">
      <p class="story-meta">Styling · 6 min read</p><h3><a href="#">Living with forty houseplants and still having room for a sofa</a></h3>
    </div>
    <div class="story" role="group" aria-roledescription="slide" aria-label="2 of 5">
      <img src="https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=700&h=460&q=70" width="700" height="460" alt="Minimal white room with a black sofa, a chest of drawers and a floor lamp">
      <p class="story-meta">Small spaces · 4 min read</p><h3><a href="#">One sofa, three rooms: furnishing a 28 m² studio</a></h3>
    </div>
    <div class="story" role="group" aria-roledescription="slide" aria-label="3 of 5">
      <img src="https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=700&h=460&q=70" width="700" height="460" alt="Open-plan flat with a red armchair and a pale wooden dining table">
      <p class="story-meta">Colour · 5 min read</p><h3><a href="#">Why one red chair works in an all-white flat</a></h3>
    </div>
    <div class="story" role="group" aria-roledescription="slide" aria-label="4 of 5">
      <img src="https://images.unsplash.com/photo-1484154218962-a197022b5858?auto=format&fit=crop&w=700&h=460&q=70" width="700" height="460" loading="lazy" alt="White kitchen with black pendant lights and bar stools at an island">
      <p class="story-meta">Kitchens · 8 min read</p><h3><a href="#">The case for open shelving, from people who tried it</a></h3>
    </div>
    <div class="story" role="group" aria-roledescription="slide" aria-label="5 of 5">
      <img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=700&h=460&q=70" width="700" height="460" loading="lazy" alt="Modern house with a timber and black metal facade">
      <p class="story-meta">Architecture · 7 min read</p><h3><a href="#">A timber house built in eleven weeks</a></h3>
    </div>
  </div>
  <button class="journal-btn" type="button" data-prev data-controls hidden aria-label="Previous stories"><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="journal-btn" type="button" data-next data-controls hidden aria-label="Next stories"><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
/* Card sliders: several cards per view on a CSS scroll-snap track. The buttons page by a full view
   and are revealed by the script; without it the row still scrolls and swipes. */
[data-controls][hidden]{display:none}
.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.shelf{--shelf-per:4;--shelf-gap:20px;--shelf-ink:#1c1917;--shelf-muted:#57534e;--shelf-accent:#15803d;--shelf-tile:#e7e2da}
.shelf-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px 24px;flex-wrap:wrap;margin-bottom:22px}
.shelf-kicker{font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--shelf-accent)}
.shelf-head h2{font-size:clamp(1.45rem,2.4vw,1.9rem);line-height:1.15;letter-spacing:-.02em;margin-top:4px}
.shelf-actions{display:flex;align-items:center;gap:10px}
.shelf-all{margin-right:8px;color:var(--shelf-ink);font-size:.9rem;font-weight:600;text-underline-offset:4px}
.shelf-btn{width:44px;height:44px;display:grid;place-items:center;border:1.5px solid #78716c;border-radius:50%;
  background:transparent;color:var(--shelf-ink);cursor:pointer;transition:background .2s,color .2s,border-color .2s}
.shelf-btn:hover{background:var(--shelf-ink);border-color:var(--shelf-ink);color:#fff}
.shelf-btn[aria-disabled=true]{opacity:.4;cursor:default;background:transparent;color:var(--shelf-ink);border-color:#78716c}
.shelf-btn svg{width:18px;height:18px}
.shelf-track{display:flex;gap:var(--shelf-gap);overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;padding:4px 4px 8px;margin:-4px -4px 0;scroll-padding-inline:4px}
.shelf.is-ready .shelf-track{scrollbar-width:none}
.item{position:relative;flex:0 0 calc((100% - (var(--shelf-per) - 1) * var(--shelf-gap)) / var(--shelf-per));scroll-snap-align:start}
.item-media{position:relative;aspect-ratio:4/5;border-radius:16px;overflow:hidden;background:var(--shelf-tile)}
.item-media img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .45s}
.item:hover .item-media img{transform:scale(1.04)}
.item-badge{position:absolute;top:12px;left:12px;padding:5px 10px;border-radius:999px;background:#fff;color:var(--shelf-ink);font-size:.72rem;font-weight:700}
.item-badge.sale{background:#b91c1c;color:#fff}
.item-name{margin-top:14px;font-size:1rem;font-weight:600;line-height:1.3}
.item-name a{color:inherit;text-decoration:none}
.item-name a::after{content:"";position:absolute;inset:0;border-radius:16px}
.item-meta{margin-top:3px;font-size:.86rem;color:var(--shelf-muted)}
.item-price{margin-top:8px;font-weight:700}
.item-price del{margin-left:6px;font-weight:400;color:var(--shelf-muted)}
.shelf-all:focus-visible,.shelf-btn:focus-visible,.item-name a:focus-visible::after{outline:3px solid var(--shelf-accent);outline-offset:3px}
.item-name a:focus-visible{outline:none}
.shelf-track:focus-visible{outline:3px solid var(--shelf-accent);outline-offset:-3px}
@media (max-width:900px){.shelf{--shelf-per:2.4}}
@media (max-width:560px){.shelf{--shelf-per:1.35;--shelf-gap:14px}}

/* Editorial cards: three and a bit per view, so the cut-off card says "there is more" */
.journal{--jr-per:3.3;--jr-gap:22px;--jr-img:220px;--jr-accent:#1d4ed8;
  position:relative;padding:32px;border-radius:24px;background:#fff;box-shadow:0 1px 2px rgba(28,25,23,.06),0 12px 32px rgba(28,25,23,.06)}
.journal h2{font-size:1.35rem;letter-spacing:-.02em;margin-bottom:20px}
.journal-track{display:flex;gap:var(--jr-gap);overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;padding-bottom:6px}
.journal.is-ready .journal-track{scrollbar-width:none}
.story{position:relative;flex:0 0 calc((100% - (var(--jr-per) - 1) * var(--jr-gap)) / var(--jr-per));scroll-snap-align:start}
.story img{width:100%;height:var(--jr-img);object-fit:cover;border-radius:14px;display:block}
.story-meta{margin-top:14px;font-size:.78rem;font-weight:600;letter-spacing:.04em;color:#475569}
.story h3{margin-top:6px;font-size:1.08rem;line-height:1.35;letter-spacing:-.01em}
.story h3 a{color:#0f172a;text-decoration:none}
.story h3 a::after{content:"";position:absolute;inset:0;border-radius:14px}
.story:hover h3 a{text-decoration:underline;text-underline-offset:3px}
.journal-rail{position:relative}
.journal-btn{position:absolute;z-index:1;top:calc(var(--jr-img) / 2 - 22px);width:44px;height:44px;
  display:grid;place-items:center;border:0;border-radius:50%;background:#fff;color:#0f172a;cursor:pointer;
  box-shadow:0 2px 6px rgba(15,23,42,.12),0 10px 24px rgba(15,23,42,.14);transition:opacity .2s,transform .2s}
.journal-btn:hover{transform:scale(1.06)}
.journal-btn[data-prev]{left:-12px}.journal-btn[data-next]{right:-12px}
.journal-btn[aria-disabled=true]{opacity:0;pointer-events:none}
.journal-btn svg{width:18px;height:18px}
.journal-btn:focus-visible,.story h3 a:focus-visible::after{outline:3px solid var(--jr-accent);outline-offset:3px}
.journal-btn[aria-disabled=true]:focus-visible{opacity:.5}
.story h3 a:focus-visible{outline:none}
@media (max-width:900px){.journal{--jr-per:2.2}}
@media (max-width:560px){.journal{--jr-per:1.2;--jr-gap:16px;--jr-img:200px;padding:22px}}
@media (prefers-reduced-motion:reduce){.shelf-btn,.item-media img,.journal-btn{transition:none}
  .item:hover .item-media img,.journal-btn: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

HTML
<div><span class="lbl">Product slider, four per view</span>
<section class="shelf" role="region" aria-roledescription="carousel" aria-label="New this week" data-carousel>
  <div class="shelf-head d-flex flex-wrap align-items-end justify-content-between">
    <div><p class="shelf-kicker">New this week</p><h2 class="shelf-title">Made for slow mornings</h2></div>
    <div class="d-flex align-items-center gap-2"><a class="shelf-all me-2" href="#">Shop all 48</a>
      <button class="btn rounded-circle shelf-btn" type="button" data-prev data-controls hidden aria-label="Previous products"><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 shelf-btn" type="button" data-next data-controls hidden aria-label="Next products"><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 class="shelf-track" data-track>
    <div class="card" role="group" aria-roledescription="slide" aria-label="1 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1459411552884-841db9b3cc2a?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Small cactus in a terracotta pot on a pink background"><span class="badge rounded-pill text-bg-light item-badge">New</span></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Saguaro in terracotta</a></h3><p class="card-text">12 cm pot · saucer included</p><p class="card-text item-price">$18</p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="2 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1485955900006-10f4d324d411?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Succulent in a mint green glazed planter"></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Haworthia, mint planter</a></h3><p class="card-text">Glazed stoneware · 14 cm</p><p class="card-text item-price">$22</p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="3 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Plain white stoneware mug on a white table"><span class="badge rounded-pill item-badge sale">−20%</span></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Stoneware mug, chalk</a></h3><p class="card-text">350 ml · dishwasher safe</p><p class="card-text item-price">$12 <del><span class="visually-hidden">Was </span>$15</del></p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="4 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1503602642458-232111445657?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" alt="Pale birch wood stool against a blue wall"></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Birch counter stool</a></h3><p class="card-text">Solid birch · 65 cm seat</p><p class="card-text item-price">$96</p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="5 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1544816155-12df9643f363?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="Tan canvas tote bag hanging on a grey wall"></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Waxed canvas tote</a></h3><p class="card-text">Tobacco · 18 litres</p><p class="card-text item-price">$38</p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="6 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1602143407151-7111542de6e8?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="Matte sage green insulated water bottle"><span class="badge rounded-pill text-bg-light item-badge">Back in stock</span></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Insulated bottle, sage</a></h3><p class="card-text">500 ml · keeps cold 24 h</p><p class="card-text item-price">$26</p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="7 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1517705008128-361805f42e86?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="White moulded chair with wooden legs next to a plant stand"></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Moulded side chair</a></h3><p class="card-text">Recycled shell · oak legs</p><p class="card-text item-price">$120</p></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="8 of 8">
      <div class="item-media"><img src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=600&h=750&q=70" width="600" height="750" loading="lazy" alt="Bottle green velvet three-seater sofa on wooden legs"></div>
      <div class="card-body"><h3 class="card-title"><a class="stretched-link" href="#">Velvet sofa, bottle green</a></h3><p class="card-text">Three-seater · 214 cm</p><p class="card-text item-price">$1,240</p></div>
    </div>
  </div>
</section></div>

<div><span class="lbl">Editorial cards with a peek</span>
<section class="journal" role="region" aria-roledescription="carousel" aria-label="From the journal" data-carousel>
  <h2 class="journal-title">From the journal</h2>
  <div class="journal-rail">
  <div class="journal-track" data-track>
    <div class="card" role="group" aria-roledescription="slide" aria-label="1 of 5">
      <img src="https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?auto=format&fit=crop&w=700&h=460&q=70" class="card-img-top" width="700" height="460" alt="Sunny living room full of hanging and potted plants">
      <div class="card-body"><p class="story-meta">Styling · 6 min read</p><h3 class="card-title"><a class="stretched-link" href="#">Living with forty houseplants and still having room for a sofa</a></h3></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="2 of 5">
      <img src="https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=700&h=460&q=70" class="card-img-top" width="700" height="460" alt="Minimal white room with a black sofa, a chest of drawers and a floor lamp">
      <div class="card-body"><p class="story-meta">Small spaces · 4 min read</p><h3 class="card-title"><a class="stretched-link" href="#">One sofa, three rooms: furnishing a 28 m² studio</a></h3></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="3 of 5">
      <img src="https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=700&h=460&q=70" class="card-img-top" width="700" height="460" alt="Open-plan flat with a red armchair and a pale wooden dining table">
      <div class="card-body"><p class="story-meta">Colour · 5 min read</p><h3 class="card-title"><a class="stretched-link" href="#">Why one red chair works in an all-white flat</a></h3></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="4 of 5">
      <img src="https://images.unsplash.com/photo-1484154218962-a197022b5858?auto=format&fit=crop&w=700&h=460&q=70" class="card-img-top" width="700" height="460" loading="lazy" alt="White kitchen with black pendant lights and bar stools at an island">
      <div class="card-body"><p class="story-meta">Kitchens · 8 min read</p><h3 class="card-title"><a class="stretched-link" href="#">The case for open shelving, from people who tried it</a></h3></div>
    </div>
    <div class="card" role="group" aria-roledescription="slide" aria-label="5 of 5">
      <img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=700&h=460&q=70" class="card-img-top" width="700" height="460" loading="lazy" alt="Modern house with a timber and black metal facade">
      <div class="card-body"><p class="story-meta">Architecture · 7 min read</p><h3 class="card-title"><a class="stretched-link" href="#">A timber house built in eleven weeks</a></h3></div>
    </div>
  </div>
  <button class="btn rounded-circle journal-btn" type="button" data-prev data-controls hidden aria-label="Previous stories"><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 journal-btn" type="button" data-next data-controls hidden aria-label="Next stories"><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
/* Card sliders: Bootstrap .card, .badge, .btn and .stretched-link on a CSS scroll-snap track.
   Bootstrap's .carousel shows one item at a time, so a multi-card row scrolls natively instead. */
[data-controls][hidden]{display:none}
.shelf{--shelf-per:4;--shelf-gap:20px;--shelf-ink:#1c1917;--shelf-muted:#57534e;--shelf-accent:#15803d;--shelf-tile:#e7e2da}
.shelf-head{gap:14px 24px;margin-bottom:22px}
.shelf-kicker{font-size:.75rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--shelf-accent);margin:0}
.shelf-title{font-size:clamp(1.45rem,2.4vw,1.9rem);font-weight:700;line-height:1.15;letter-spacing:-.02em;margin:4px 0 0}
.shelf-all{color:var(--shelf-ink);font-size:.9rem;font-weight:600;text-underline-offset:4px}
.shelf-btn{--bs-btn-color:var(--shelf-ink);--bs-btn-border-color:#78716c;--bs-btn-border-width:1.5px;--bs-btn-hover-bg:var(--shelf-ink);
  --bs-btn-hover-border-color:var(--shelf-ink);--bs-btn-hover-color:#fff;--bs-btn-active-bg:var(--shelf-ink);--bs-btn-active-color:#fff;
  width:44px;height:44px;padding:0;display:grid;place-items:center}
.shelf-btn[aria-disabled=true]{opacity:.4;cursor:default;background:transparent;color:var(--shelf-ink);border-color:#78716c}
.shelf-btn svg{width:18px;height:18px}
.shelf-track{display:flex;gap:var(--shelf-gap);overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;padding:4px 4px 8px;margin:-4px -4px 0;scroll-padding-inline:4px}
.shelf.is-ready .shelf-track{scrollbar-width:none}
.shelf .card{--bs-card-border-width:0;--bs-card-bg:transparent;--bs-card-spacer-x:0;--bs-card-spacer-y:14px;--bs-card-title-spacer-y:0;
  flex:0 0 calc((100% - (var(--shelf-per) - 1) * var(--shelf-gap)) / var(--shelf-per));scroll-snap-align:start}
.item-media{position:relative;aspect-ratio:4/5;border-radius:16px;overflow:hidden;background:var(--shelf-tile)}
.item-media img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .45s}
.shelf .card:hover .item-media img{transform:scale(1.04)}
.item-badge{position:absolute;top:12px;left:12px;padding:5px 10px;font-size:.72rem;font-weight:700;color:var(--shelf-ink)}
.item-badge.sale{background:#b91c1c!important;color:#fff!important}
.shelf .card-body{padding-bottom:0}
.shelf .card-title{font-size:1rem;font-weight:600;line-height:1.3;margin:0}
.shelf .card-title a{color:inherit;text-decoration:none}
.shelf .card-text{margin:3px 0 0;font-size:.86rem;color:var(--shelf-muted)}
.shelf .card-text.item-price{margin-top:8px;font-size:1rem;font-weight:700;color:var(--shelf-ink)}
.item-price del{margin-left:6px;font-weight:400;color:var(--shelf-muted)}
.stretched-link::after{border-radius:16px}
.shelf-all:focus-visible,.shelf-btn:focus-visible,.shelf .stretched-link:focus-visible::after{outline:3px solid var(--shelf-accent);outline-offset:3px;box-shadow:none}
.shelf .stretched-link:focus-visible{outline:none}
@media (max-width:900px){.shelf{--shelf-per:2.4}}
@media (max-width:560px){.shelf{--shelf-per:1.35;--shelf-gap:14px}}

/* Editorial cards: three and a bit per view, so the cut-off card says "there is more" */
.journal{--jr-per:3.3;--jr-gap:22px;--jr-img:220px;--jr-accent:#1d4ed8;
  padding:32px;border-radius:24px;background:#fff;box-shadow:0 1px 2px rgba(28,25,23,.06),0 12px 32px rgba(28,25,23,.06)}
.journal-title{font-size:1.35rem;font-weight:700;letter-spacing:-.02em;margin:0 0 20px}
.journal-rail{position:relative}
.journal-track{display:flex;gap:var(--jr-gap);overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain;padding-bottom:6px}
.journal.is-ready .journal-track{scrollbar-width:none}
.journal .card{--bs-card-border-width:0;--bs-card-spacer-x:0;--bs-card-spacer-y:14px;--bs-card-inner-border-radius:14px;
  flex:0 0 calc((100% - (var(--jr-per) - 1) * var(--jr-gap)) / var(--jr-per));scroll-snap-align:start}
.journal .card-img-top{height:var(--jr-img);object-fit:cover;border-radius:14px}
.journal .card-body{padding-bottom:0}
.story-meta{margin:0;font-size:.78rem;font-weight:600;letter-spacing:.04em;color:#475569}
.journal .card-title{margin:6px 0 0;font-size:1.08rem;font-weight:700;line-height:1.35;letter-spacing:-.01em}
.journal .card-title a{color:#0f172a;text-decoration:none}
.journal .card:hover .card-title a{text-decoration:underline;text-underline-offset:3px}
.journal-btn{--bs-btn-bg:#fff;--bs-btn-color:#0f172a;--bs-btn-hover-bg:#fff;--bs-btn-hover-color:#0f172a;--bs-btn-active-bg:#fff;--bs-btn-border-width:0;
  position:absolute;z-index:2;top:calc(var(--jr-img) / 2 - 22px);width:44px;height:44px;padding:0;display:grid;place-items:center;
  box-shadow:0 2px 6px rgba(15,23,42,.12),0 10px 24px rgba(15,23,42,.14);transition:opacity .2s,transform .2s}
.journal-btn:hover{transform:scale(1.06)}
.journal-btn[data-prev]{left:-12px}.journal-btn[data-next]{right:-12px}
.journal-btn[aria-disabled=true]{opacity:0;pointer-events:none}
.journal-btn svg{width:18px;height:18px}
.journal-btn:focus-visible,.journal .stretched-link:focus-visible::after{outline:3px solid var(--jr-accent);outline-offset:3px;box-shadow:none}
.journal-btn[aria-disabled=true]:focus-visible{opacity:.5}
.journal .stretched-link:focus-visible{outline:none}
.journal .stretched-link::after{border-radius:14px}
@media (max-width:900px){.journal{--jr-per:2.2}}
@media (max-width:560px){.journal{--jr-per:1.2;--jr-gap:16px;--jr-img:200px;padding:22px}}
@media (prefers-reduced-motion:reduce){.item-media img,.journal-btn{transition:none}
  .shelf .card:hover .item-media img,.journal-btn: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();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-shelf-muted uppercase">Product slider, four per view</span>
<section class="[--gap:14px] [--per:1.35] min-[561px]:[--gap:20px] min-[561px]:[--per:2.4] min-[901px]:[--per:4]" role="region" aria-roledescription="carousel" aria-label="New this week" data-carousel>
  <div class="mb-[22px] flex flex-wrap items-end justify-between gap-x-6 gap-y-3.5">
    <div><p class="text-xs font-bold tracking-[.12em] text-shelf-accent uppercase">New this week</p><h2 class="mt-1 text-[clamp(1.45rem,2.4vw,1.9rem)] leading-[1.15] font-bold tracking-[-.02em]">Made for slow mornings</h2></div>
    <div class="flex items-center gap-2.5"><a class="mr-2 text-[.9rem] font-semibold text-shelf-ink underline underline-offset-4 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-shelf-accent" href="#">Shop all 48</a>
      <button class="grid size-11 cursor-pointer place-items-center rounded-full border-[1.5px] border-[#78716c] text-shelf-ink transition-[background-color,color,border-color] duration-200 hover:border-shelf-ink hover:bg-shelf-ink hover:text-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-shelf-accent aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:border-[#78716c] aria-disabled:hover:bg-transparent aria-disabled:hover:text-shelf-ink motion-reduce:transition-none" type="button" data-prev data-controls hidden aria-label="Previous products"><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-[#78716c] text-shelf-ink transition-[background-color,color,border-color] duration-200 hover:border-shelf-ink hover:bg-shelf-ink hover:text-white focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-shelf-accent aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:border-[#78716c] aria-disabled:hover:bg-transparent aria-disabled:hover:text-shelf-ink motion-reduce:transition-none" type="button" data-next data-controls hidden aria-label="Next products"><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>
  <div class="-mx-1 -mt-1 flex snap-x snap-mandatory scroll-px-1 gap-(--gap) overflow-x-auto overscroll-x-contain px-1 pt-1 pb-2 in-[.is-ready]:[scrollbar-width:none]" data-track>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="1 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1459411552884-841db9b3cc2a?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" alt="Small cactus in a terracotta pot on a pink background"><span class="absolute top-3 left-3 rounded-full px-2.5 py-[5px] text-[.72rem] font-bold bg-white text-shelf-ink">New</span></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Saguaro in terracotta</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">12 cm pot · saucer included</p><p class="mt-2 font-bold">$18</p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="2 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1485955900006-10f4d324d411?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" alt="Succulent in a mint green glazed planter"></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Haworthia, mint planter</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">Glazed stoneware · 14 cm</p><p class="mt-2 font-bold">$22</p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="3 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" alt="Plain white stoneware mug on a white table"><span class="absolute top-3 left-3 rounded-full px-2.5 py-[5px] text-[.72rem] font-bold bg-[#b91c1c] text-white">−20%</span></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Stoneware mug, chalk</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">350 ml · dishwasher safe</p><p class="mt-2 font-bold">$12 <del class="ml-1.5 font-normal text-shelf-muted"><span class="sr-only">Was </span>$15</del></p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="4 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1503602642458-232111445657?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" alt="Pale birch wood stool against a blue wall"></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Birch counter stool</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">Solid birch · 65 cm seat</p><p class="mt-2 font-bold">$96</p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="5 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1544816155-12df9643f363?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" loading="lazy" alt="Tan canvas tote bag hanging on a grey wall"></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Waxed canvas tote</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">Tobacco · 18 litres</p><p class="mt-2 font-bold">$38</p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="6 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1602143407151-7111542de6e8?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" loading="lazy" alt="Matte sage green insulated water bottle"><span class="absolute top-3 left-3 rounded-full px-2.5 py-[5px] text-[.72rem] font-bold bg-white text-shelf-ink">Back in stock</span></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Insulated bottle, sage</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">500 ml · keeps cold 24 h</p><p class="mt-2 font-bold">$26</p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="7 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1517705008128-361805f42e86?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" loading="lazy" alt="White moulded chair with wooden legs next to a plant stand"></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Moulded side chair</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">Recycled shell · oak legs</p><p class="mt-2 font-bold">$120</p>
    </div>
    <div class="group/item relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="8 of 8">
      <div class="relative aspect-[4/5] overflow-hidden rounded-2xl bg-shelf-tile"><img src="https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=600&h=750&q=70" class="block size-full object-cover transition-transform duration-[450ms] group-hover/item:scale-104 motion-reduce:transition-none motion-reduce:group-hover/item:scale-100" width="600" height="750" loading="lazy" alt="Bottle green velvet three-seater sofa on wooden legs"></div>
      <h3 class="mt-3.5 text-base leading-[1.3] font-semibold"><a class="after:absolute after:inset-0 after:rounded-2xl focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-shelf-accent" href="#">Velvet sofa, bottle green</a></h3><p class="mt-[3px] text-[.86rem] text-shelf-muted">Three-seater · 214 cm</p><p class="mt-2 font-bold">$1,240</p>
    </div>
  </div>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-shelf-muted uppercase">Editorial cards with a peek</span>
<section class="rounded-3xl bg-white p-[22px] shadow-[0_1px_2px_rgba(28,25,23,.06),0_12px_32px_rgba(28,25,23,.06)] [--gap:16px] [--img:200px] [--per:1.2] min-[561px]:p-8 min-[561px]:[--gap:22px] min-[561px]:[--img:220px] min-[561px]:[--per:2.2] min-[901px]:[--per:3.3]" role="region" aria-roledescription="carousel" aria-label="From the journal" data-carousel>
  <h2 class="mb-5 text-[1.35rem] font-bold tracking-[-.02em]">From the journal</h2>
  <div class="relative">
  <div class="flex snap-x snap-mandatory gap-(--gap) overflow-x-auto overscroll-x-contain pb-1.5 in-[.is-ready]:[scrollbar-width:none]" data-track>
    <div class="group/story relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="1 of 5">
      <img src="https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?auto=format&fit=crop&w=700&h=460&q=70" class="block h-(--img) w-full rounded-[14px] object-cover" width="700" height="460" alt="Sunny living room full of hanging and potted plants">
      <p class="mt-3.5 text-[.78rem] font-semibold tracking-[.04em] text-[#475569]">Styling · 6 min read</p><h3 class="mt-1.5 text-[1.08rem] leading-[1.35] font-bold tracking-[-.01em]"><a class="text-[#0f172a] underline-offset-3 group-hover/story:underline after:absolute after:inset-0 after:rounded-[14px] focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-[#1d4ed8]" href="#">Living with forty houseplants and still having room for a sofa</a></h3>
    </div>
    <div class="group/story relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="2 of 5">
      <img src="https://images.unsplash.com/photo-1513694203232-719a280e022f?auto=format&fit=crop&w=700&h=460&q=70" class="block h-(--img) w-full rounded-[14px] object-cover" width="700" height="460" alt="Minimal white room with a black sofa, a chest of drawers and a floor lamp">
      <p class="mt-3.5 text-[.78rem] font-semibold tracking-[.04em] text-[#475569]">Small spaces · 4 min read</p><h3 class="mt-1.5 text-[1.08rem] leading-[1.35] font-bold tracking-[-.01em]"><a class="text-[#0f172a] underline-offset-3 group-hover/story:underline after:absolute after:inset-0 after:rounded-[14px] focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-[#1d4ed8]" href="#">One sofa, three rooms: furnishing a 28 m² studio</a></h3>
    </div>
    <div class="group/story relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="3 of 5">
      <img src="https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&w=700&h=460&q=70" class="block h-(--img) w-full rounded-[14px] object-cover" width="700" height="460" alt="Open-plan flat with a red armchair and a pale wooden dining table">
      <p class="mt-3.5 text-[.78rem] font-semibold tracking-[.04em] text-[#475569]">Colour · 5 min read</p><h3 class="mt-1.5 text-[1.08rem] leading-[1.35] font-bold tracking-[-.01em]"><a class="text-[#0f172a] underline-offset-3 group-hover/story:underline after:absolute after:inset-0 after:rounded-[14px] focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-[#1d4ed8]" href="#">Why one red chair works in an all-white flat</a></h3>
    </div>
    <div class="group/story relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="4 of 5">
      <img src="https://images.unsplash.com/photo-1484154218962-a197022b5858?auto=format&fit=crop&w=700&h=460&q=70" class="block h-(--img) w-full rounded-[14px] object-cover" width="700" height="460" loading="lazy" alt="White kitchen with black pendant lights and bar stools at an island">
      <p class="mt-3.5 text-[.78rem] font-semibold tracking-[.04em] text-[#475569]">Kitchens · 8 min read</p><h3 class="mt-1.5 text-[1.08rem] leading-[1.35] font-bold tracking-[-.01em]"><a class="text-[#0f172a] underline-offset-3 group-hover/story:underline after:absolute after:inset-0 after:rounded-[14px] focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-[#1d4ed8]" href="#">The case for open shelving, from people who tried it</a></h3>
    </div>
    <div class="group/story relative shrink-0 basis-[calc((100%-(var(--per)-1)*var(--gap))/var(--per))] snap-start" role="group" aria-roledescription="slide" aria-label="5 of 5">
      <img src="https://images.unsplash.com/photo-1600566753190-17f0baa2a6c3?auto=format&fit=crop&w=700&h=460&q=70" class="block h-(--img) w-full rounded-[14px] object-cover" width="700" height="460" loading="lazy" alt="Modern house with a timber and black metal facade">
      <p class="mt-3.5 text-[.78rem] font-semibold tracking-[.04em] text-[#475569]">Architecture · 7 min read</p><h3 class="mt-1.5 text-[1.08rem] leading-[1.35] font-bold tracking-[-.01em]"><a class="text-[#0f172a] underline-offset-3 group-hover/story:underline after:absolute after:inset-0 after:rounded-[14px] focus-visible:outline-none focus-visible:after:outline-3 focus-visible:after:outline-offset-3 focus-visible:after:outline-[#1d4ed8]" href="#">A timber house built in eleven weeks</a></h3>
    </div>
  </div>
  <button class="absolute top-[calc(var(--img)/2-22px)] z-[1] grid size-11 cursor-pointer place-items-center rounded-full bg-white text-[#0f172a] shadow-[0_2px_6px_rgba(15,23,42,.12),0_10px_24px_rgba(15,23,42,.14)] transition-[opacity,transform] duration-200 hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#1d4ed8] aria-disabled:pointer-events-none aria-disabled:opacity-0 aria-disabled:focus-visible:opacity-50 motion-reduce:transition-none motion-reduce:hover:scale-100 -left-3" type="button" data-prev data-controls hidden aria-label="Previous stories"><svg class="size-[18px]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6"/></svg></button>
  <button class="absolute top-[calc(var(--img)/2-22px)] z-[1] grid size-11 cursor-pointer place-items-center rounded-full bg-white text-[#0f172a] shadow-[0_2px_6px_rgba(15,23,42,.12),0_10px_24px_rgba(15,23,42,.14)] transition-[opacity,transform] duration-200 hover:scale-106 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#1d4ed8] aria-disabled:pointer-events-none aria-disabled:opacity-0 aria-disabled:focus-visible:opacity-50 motion-reduce:transition-none motion-reduce:hover:scale-100 -right-3" type="button" data-next data-controls hidden aria-label="Next stories"><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-shelf-ink: #1c1917;
  --color-shelf-muted: #57534e;
  --color-shelf-accent: #15803d;
  --color-shelf-tile: #e7e2da;
}
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

Sliders that show several cards at once. Product slider, four per view is a shop row with a small heading, a Shop all link and two round arrow buttons in the header, and eight product cards with a photo, an optional badge such as New or −20%, a name, a detail line and a price, including a crossed-out old price. Editorial cards with a peek shows three and a bit journal cards on a white panel, so the cut-off card tells people there is more, with arrows floating over the photos. Both use a CSS scroll-snap track, and the number of cards per view is one custom property that drops to 2.4 on tablets and about 1.3 on phones. The arrows page by a full view, not by one card, and switch to aria-disabled at either end, so keyboard focus stays on them. Each card is one link: the title’s link stretches over the whole card. The Bootstrap version uses .card, .badge, .btn and .stretched-link on the same scroll-snap row, because Bootstrap’s .carousel can only show one item at a time.

What’s included

  • Four, three or any number of cards per view from one custom property
  • Arrows page by a full view and dim at either end
  • Whole card is clickable through a stretched title link
  • Sale price with a crossed-out old price that screen readers read as "Was $15"
  • Bootstrap version built from .card, .badge, .btn and .stretched-link

Accessibility

Each slider is a labelled region with aria-roledescription="carousel", each card a group labelled "3 of 8", and the arrows are named buttons such as "Next products". At the ends the arrows get aria-disabled rather than disabled, so a keyboard user does not lose focus. Links and arrows show a 3px outline on focus, the card outline follows the rounded corners, and the image zoom and button transitions stop under prefers-reduced-motion.

Customise it

Set --shelf-per, --shelf-gap, --shelf-accent and --shelf-tile on .shelf, and --jr-per, --jr-gap and --jr-img on .journal. In Tailwind the same values are the --per and --gap arbitrary properties on the section, with the colours in @theme.