<div><span class="lbl">Card slider with arrow buttons</span>
<section class="sl" data-slider aria-labelledby="sl-a-title">
<div class="sl-head">
<div><p class="sl-kicker">Reviews</p><h2 id="sl-a-title">Loved by 2,400 independent shops</h2></div>
<div class="sl-controls" data-controls hidden>
<button class="sl-btn" type="button" data-prev aria-label="Previous reviews"><svg viewBox="0 0 24 24" width="20" height="20" 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="sl-btn" type="button" data-next aria-label="Next reviews"><svg viewBox="0 0 24 24" width="20" height="20" 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>
<ul class="sl-track" data-track tabindex="0" role="list" aria-label="Customer reviews, scroll sideways">
<li><figure class="sl-card">
<span class="sl-tag">Bakery · Porto</span>
<blockquote><p>Saturday queues used to reach the corner. Now we take payment at the counter and on a tablet by the door, and the line moves twice as fast.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#c2410c" aria-hidden="true">RC</span><span><span class="sl-name">Rita Carvalho</span><span class="sl-role">Owner, Massa Mãe</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<span class="sl-tag">Bookshop · Leeds</span>
<blockquote><p>Stock counts used to close the shop for a day each quarter. We now do them in the evening with two phones and a cup of tea.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#1d4ed8" aria-hidden="true">OP</span><span><span class="sl-name">Owen Pritchard</span><span class="sl-role">Manager, Second Chapter Books</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<span class="sl-tag">Florist · Utrecht</span>
<blockquote><p>Wedding deposits, delivery slots and walk-in sales all land in one report. My accountant sent me a thank-you card.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#be123c" aria-hidden="true">FW</span><span><span class="sl-name">Femke de Wit</span><span class="sl-role">Founder, Wild Stems</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<span class="sl-tag">Bike shop · Denver</span>
<blockquote><p>Repairs get a ticket, a text when they are ready, and a receipt that lists every part. Customers stopped calling to ask.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#15803d" aria-hidden="true">MR</span><span><span class="sl-name">Marco Ruiz</span><span class="sl-role">Co-owner, Spoke & Chain</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<span class="sl-tag">Café · Melbourne</span>
<blockquote><p>I set up the menu, the tips and the kitchen printer myself on a Sunday night. Monday breakfast ran without a hiccup.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#7e22ce" aria-hidden="true">GN</span><span><span class="sl-name">Grace Nguyen</span><span class="sl-role">Owner, Little Ember</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<span class="sl-tag">Record store · Austin</span>
<blockquote><p>Used vinyl is all one-offs. Scanning a sleeve and setting a price takes ten seconds, and the website updates on its own.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#0369a1" aria-hidden="true">DB</span><span><span class="sl-name">Darnell Brooks</span><span class="sl-role">Owner, Crate Diggers</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<span class="sl-tag">Deli · Kraków</span>
<blockquote><p>We sell by weight, by slice and by the jar. It handles all three, and the scale talks to the till without a cable mess.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#a16207" aria-hidden="true">ZN</span><span><span class="sl-name">Zofia Nowak</span><span class="sl-role">Owner, Spiżarnia</span></span></figcaption>
</figure></li>
</ul>
</section></div>
<div><span class="lbl">Split slider, one story at a time</span>
<section class="sl sl--split" data-slider aria-labelledby="sl-b-title">
<div class="sl-intro">
<p class="sl-kicker">Customer stories</p>
<h2 id="sl-b-title">Stories from the shop floor</h2>
<p>Owners on what changed in their first month with Tillwise.</p>
<div class="sl-controls" data-controls hidden>
<button class="sl-btn" type="button" data-prev aria-label="Previous story"><svg viewBox="0 0 24 24" width="20" height="20" 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="sl-btn" type="button" data-next aria-label="Next story"><svg viewBox="0 0 24 24" width="20" height="20" 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>
<span class="sl-count" data-count>01 / 04</span>
</div>
</div>
<ul class="sl-track" data-track tabindex="0" role="list" aria-label="Customer stories, scroll sideways">
<li><figure class="sl-card">
<blockquote><p>The first week we switched, we found out which of our cakes actually sold. Two left the menu, and Saturday waste dropped by a third.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#c2410c" aria-hidden="true">HS</span><span><span class="sl-name">Helena Sørensen</span><span class="sl-role">Owner, Kanel Bakehouse · customer since 2021</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<blockquote><p>Three shops, one stock list. When the Leith branch runs out, the staff can see the other two still have it and send the customer over.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#1d4ed8" aria-hidden="true">CR</span><span><span class="sl-name">Callum Reid</span><span class="sl-role">Director, Reid & Daughters Hardware</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<blockquote><p>I was sure we would need an IT person. We needed a free afternoon and the setup guide, and that was it.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#15803d" aria-hidden="true">AB</span><span><span class="sl-name">Ama Boateng</span><span class="sl-role">Owner, Kente Corner · customer since 2022</span></span></figcaption>
</figure></li>
<li><figure class="sl-card">
<blockquote><p>End of day used to mean counting coins at midnight. Now the till balances itself and I am home before the kids are asleep.</p></blockquote>
<figcaption><span class="sl-av" style="--av:#be123c" aria-hidden="true">RC</span><span><span class="sl-name">Rafael Costa</span><span class="sl-role">Owner, Costa Gelato · customer since 2020</span></span></figcaption>
</figure></li>
</ul>
</section></div>
CSS
/* Testimonial sliders: a scroll-snap list of <figure> cards. It swipes on touch, scrolls with a trackpad
and takes arrow keys once focused, with or without the script; the script adds the buttons. */
.sl{--sl-ink:#1f2937;--sl-muted:#4b5563;--sl-line:#e7e2da;--sl-accent:#c2410c}
.sl-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px 24px;margin-bottom:24px}
.sl-kicker{font-size:.8rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--sl-accent)}
.sl h2{margin-top:6px;font-size:clamp(1.5rem,1.1rem + 1.5vw,2rem);font-weight:800;line-height:1.15;letter-spacing:-.02em;color:var(--sl-ink)}
.sl-controls{display:flex;align-items:center;gap:10px}
.sl-controls[hidden]{display:none}
.sl-btn{display:grid;place-items:center;width:44px;height:44px;padding:0;border:1.5px solid #6b7280;border-radius:50%;
background:#fff;color:var(--sl-ink);cursor:pointer;transition:background-color .2s,color .2s,border-color .2s,opacity .2s}
.sl-btn:hover{background:var(--sl-ink);border-color:var(--sl-ink);color:#fff}
.sl-btn[aria-disabled=true]{opacity:.4;cursor:default;background:#fff;border-color:#6b7280;color:var(--sl-ink)}
.sl-btn:focus-visible{outline:3px solid var(--sl-accent);outline-offset:2px}
.sl-count{min-width:4.5ch;font-size:.95rem;font-weight:700;font-variant-numeric:tabular-nums;color:var(--sl-muted)}
.sl-track{display:flex;gap:20px;margin:0;padding:4px 2px 18px;list-style:none;overflow-x:auto;
scroll-snap-type:x mandatory;overscroll-behavior-x:contain;scrollbar-width:none}
.sl-track::-webkit-scrollbar{display:none}
.sl-track:focus-visible{outline:3px solid var(--sl-accent);outline-offset:4px;border-radius:20px}
.sl-track>li{display:flex;flex:0 0 86%;scroll-snap-align:start}
@media (min-width:768px){.sl-track>li{flex-basis:calc((100% - 20px) / 2)}}
@media (min-width:992px){.sl-track>li{flex-basis:calc((100% - 40px) / 3)}}
.sl-card{flex:1;display:flex;flex-direction:column;gap:18px;margin:0;padding:26px;background:#fff;border:1px solid var(--sl-line);
border-radius:20px;box-shadow:0 10px 24px -18px rgba(31,41,55,.35)}
.sl-tag{align-self:flex-start;padding:4px 10px;border-radius:999px;background:#fff1e6;font-size:.78rem;font-weight:700;color:#9a3412}
.sl-card blockquote{flex:1;margin:0;font-size:1rem;line-height:1.65;color:var(--sl-ink)}
.sl-card figcaption{display:flex;align-items:center;gap:12px}
.sl-av{flex:none;display:grid;place-items:center;width:42px;height:42px;border-radius:14px;background:var(--av,#c2410c);
color:#fff;font-size:.85rem;font-weight:700;letter-spacing:.03em}
.sl-name{display:block;font-weight:700;color:var(--sl-ink)}
.sl-role{display:block;font-size:.875rem;line-height:1.4;color:var(--sl-muted)}
/* Split slider: heading and controls on the left, one quote at a time on the right */
.sl--split{display:grid;gap:24px;align-items:center;padding:clamp(24px,4vw,48px);border-radius:28px;background:#fff7ed;border:1px solid #fde4cc}
@media (min-width:768px){.sl--split{grid-template-columns:minmax(0,5fr) minmax(0,7fr)}}
.sl--split .sl-intro p:not(.sl-kicker){margin-top:12px;color:var(--sl-muted)}
.sl--split .sl-controls{margin-top:24px}
.sl--split .sl-track{padding:4px 2px 20px;margin-bottom:-16px}
.sl--split .sl-track>li{flex-basis:100%}
.sl--split .sl-card{gap:24px;padding:clamp(24px,3.5vw,40px);border-color:#fde4cc;box-shadow:0 18px 40px -26px rgba(154,52,18,.45)}
.sl--split .sl-card blockquote{font-size:clamp(1.1rem,1rem + .5vw,1.3rem);font-weight:500;line-height:1.55}
.sl--split .sl-card blockquote::before{content:"\201C";content:"\201C" / "";display:block;height:34px;font:700 3.6rem/1 Georgia,"Times New Roman",serif;color:var(--sl-accent)}
.sl--split .sl-av{width:48px;height:48px;border-radius:50%}
@media (prefers-reduced-motion:reduce){.sl-btn{transition:none}}
JavaScript
// Testimonial slider: the track is a CSS scroll-snap list that swipes, scrolls and takes arrow keys
// without any script. The script only reveals the buttons, moves one card per click, marks a button
// aria-disabled at either end and keeps the optional counter in step. Nothing moves on its own.
document.querySelectorAll('[data-slider]').forEach((root) => {
const track = root.querySelector('[data-track]');
const prev = root.querySelector('[data-prev]');
const next = root.querySelector('[data-next]');
const count = root.querySelector('[data-count]');
const slides = [...track.children];
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const pad = (n) => String(n).padStart(2, '0');
const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
const sync = () => {
const end = track.scrollWidth - track.clientWidth - 2;
prev.setAttribute('aria-disabled', track.scrollLeft <= 2);
next.setAttribute('aria-disabled', track.scrollLeft >= end);
if (count) count.textContent = pad(Math.round(track.scrollLeft / step()) + 1) + ' / ' + pad(slides.length);
};
const go = (dir, btn) => {
if (btn.getAttribute('aria-disabled') === 'true') return;
track.scrollBy({ left: dir * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
};
prev.addEventListener('click', () => go(-1, prev));
next.addEventListener('click', () => go(1, next));
track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
window.addEventListener('resize', sync);
root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
sync();
});
Bootstrap 5 code
Requires: Bootstrap 5.3.8 CSS
HTML
<div><span class="lbl">Card slider with arrow buttons</span>
<section class="sl" data-slider aria-labelledby="sl-a-title">
<div class="d-flex flex-wrap align-items-end justify-content-between column-gap-4 row-gap-3 mb-4">
<div><p class="sl-kicker mb-0">Reviews</p><h2 id="sl-a-title">Loved by 2,400 independent shops</h2></div>
<div class="sl-controls" data-controls hidden>
<button class="btn sl-btn" type="button" data-prev aria-label="Previous reviews"><svg viewBox="0 0 24 24" width="20" height="20" 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 sl-btn" type="button" data-next aria-label="Next reviews"><svg viewBox="0 0 24 24" width="20" height="20" 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>
<ul class="sl-track d-flex list-unstyled mb-0" data-track tabindex="0" role="list" aria-label="Customer reviews, scroll sideways">
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Bakery · Porto</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">Saturday queues used to reach the corner. Now we take payment at the counter and on a tablet by the door, and the line moves twice as fast.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#c2410c" aria-hidden="true">RC</span><span><span class="d-block fw-bold">Rita Carvalho</span><span class="d-block sl-role">Owner, Massa Mãe</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Bookshop · Leeds</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">Stock counts used to close the shop for a day each quarter. We now do them in the evening with two phones and a cup of tea.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#1d4ed8" aria-hidden="true">OP</span><span><span class="d-block fw-bold">Owen Pritchard</span><span class="d-block sl-role">Manager, Second Chapter Books</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Florist · Utrecht</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">Wedding deposits, delivery slots and walk-in sales all land in one report. My accountant sent me a thank-you card.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#be123c" aria-hidden="true">FW</span><span><span class="d-block fw-bold">Femke de Wit</span><span class="d-block sl-role">Founder, Wild Stems</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Bike shop · Denver</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">Repairs get a ticket, a text when they are ready, and a receipt that lists every part. Customers stopped calling to ask.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#15803d" aria-hidden="true">MR</span><span><span class="d-block fw-bold">Marco Ruiz</span><span class="d-block sl-role">Co-owner, Spoke & Chain</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Café · Melbourne</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">I set up the menu, the tips and the kitchen printer myself on a Sunday night. Monday breakfast ran without a hiccup.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#7e22ce" aria-hidden="true">GN</span><span><span class="d-block fw-bold">Grace Nguyen</span><span class="d-block sl-role">Owner, Little Ember</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Record store · Austin</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">Used vinyl is all one-offs. Scanning a sleeve and setting a price takes ten seconds, and the website updates on its own.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#0369a1" aria-hidden="true">DB</span><span><span class="d-block fw-bold">Darnell Brooks</span><span class="d-block sl-role">Owner, Crate Diggers</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<span class="badge rounded-pill sl-tag align-self-start">Deli · Kraków</span>
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">We sell by weight, by slice and by the jar. It handles all three, and the scale talks to the till without a cable mess.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#a16207" aria-hidden="true">ZN</span><span><span class="d-block fw-bold">Zofia Nowak</span><span class="d-block sl-role">Owner, Spiżarnia</span></span></figcaption>
</div></figure></li>
</ul>
</section></div>
<div><span class="lbl">Split slider, one story at a time</span>
<section class="sl sl--split" data-slider aria-labelledby="sl-b-title">
<div class="row g-4 align-items-center">
<div class="col-md-5">
<p class="sl-kicker mb-0">Customer stories</p>
<h2 id="sl-b-title">Stories from the shop floor</h2>
<p class="mt-3 mb-0 sl-role fs-6">Owners on what changed in their first month with Tillwise.</p>
<div class="sl-controls mt-4" data-controls hidden>
<button class="btn sl-btn" type="button" data-prev aria-label="Previous story"><svg viewBox="0 0 24 24" width="20" height="20" 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 sl-btn" type="button" data-next aria-label="Next story"><svg viewBox="0 0 24 24" width="20" height="20" 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>
<span class="sl-count" data-count>01 / 04</span>
</div>
</div>
<div class="col-md-7">
<ul class="sl-track d-flex list-unstyled mb-0" data-track tabindex="0" role="list" aria-label="Customer stories, scroll sideways">
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">The first week we switched, we found out which of our cakes actually sold. Two left the menu, and Saturday waste dropped by a third.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#c2410c" aria-hidden="true">HS</span><span><span class="d-block fw-bold">Helena Sørensen</span><span class="d-block sl-role">Owner, Kanel Bakehouse · customer since 2021</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">Three shops, one stock list. When the Leith branch runs out, the staff can see the other two still have it and send the customer over.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#1d4ed8" aria-hidden="true">CR</span><span><span class="d-block fw-bold">Callum Reid</span><span class="d-block sl-role">Director, Reid & Daughters Hardware</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">I was sure we would need an IT person. We needed a free afternoon and the setup guide, and that was it.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#15803d" aria-hidden="true">AB</span><span><span class="d-block fw-bold">Ama Boateng</span><span class="d-block sl-role">Owner, Kente Corner · customer since 2022</span></span></figcaption>
</div></figure></li>
<li class="d-flex"><figure class="card sl-card flex-fill mb-0"><div class="card-body d-flex flex-column">
<blockquote class="flex-grow-1 mb-0"><p class="mb-0">End of day used to mean counting coins at midnight. Now the till balances itself and I am home before the kids are asleep.</p></blockquote>
<figcaption class="d-flex align-items-center gap-3"><span class="sl-av" style="--av:#be123c" aria-hidden="true">RC</span><span><span class="d-block fw-bold">Rafael Costa</span><span class="d-block sl-role">Owner, Costa Gelato · customer since 2020</span></span></figcaption>
</div></figure></li>
</ul>
</div>
</div>
</section></div>
// Testimonial slider: the track is a CSS scroll-snap list that swipes, scrolls and takes arrow keys
// without any script. The script only reveals the buttons, moves one card per click, marks a button
// aria-disabled at either end and keeps the optional counter in step. Nothing moves on its own.
document.querySelectorAll('[data-slider]').forEach((root) => {
const track = root.querySelector('[data-track]');
const prev = root.querySelector('[data-prev]');
const next = root.querySelector('[data-next]');
const count = root.querySelector('[data-count]');
const slides = [...track.children];
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const pad = (n) => String(n).padStart(2, '0');
const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
const sync = () => {
const end = track.scrollWidth - track.clientWidth - 2;
prev.setAttribute('aria-disabled', track.scrollLeft <= 2);
next.setAttribute('aria-disabled', track.scrollLeft >= end);
if (count) count.textContent = pad(Math.round(track.scrollLeft / step()) + 1) + ' / ' + pad(slides.length);
};
const go = (dir, btn) => {
if (btn.getAttribute('aria-disabled') === 'true') return;
track.scrollBy({ left: dir * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
};
prev.addEventListener('click', () => go(-1, prev));
next.addEventListener('click', () => go(1, next));
track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
window.addEventListener('resize', sync);
root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
sync();
});
Tailwind 4 code
Requires: Tailwind CSS 4
HTML
<div><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-[#4b5563] uppercase">Card slider with arrow buttons</span>
<section data-slider aria-labelledby="sl-a-title">
<div class="mb-6 flex flex-wrap items-end justify-between gap-x-6 gap-y-4">
<div><p class="text-[.8rem] font-bold tracking-[.12em] text-[#c2410c] uppercase">Reviews</p><h2 id="sl-a-title" class="mt-1.5 text-[clamp(1.5rem,1.1rem+1.5vw,2rem)] leading-[1.15] font-extrabold tracking-[-.02em] text-[#1f2937]">Loved by 2,400 independent shops</h2></div>
<div class="flex items-center gap-2.5" data-controls hidden>
<button class="grid size-11 place-items-center rounded-full border-[1.5px] border-[#6b7280] bg-white text-[#1f2937] cursor-pointer transition-colors duration-200 hover:border-[#1f2937] hover:bg-[#1f2937] hover:text-white aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:border-[#6b7280] aria-disabled:hover:bg-white aria-disabled:hover:text-[#1f2937] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#c2410c] motion-reduce:transition-none" type="button" data-prev aria-label="Previous reviews"><svg viewBox="0 0 24 24" width="20" height="20" 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 place-items-center rounded-full border-[1.5px] border-[#6b7280] bg-white text-[#1f2937] cursor-pointer transition-colors duration-200 hover:border-[#1f2937] hover:bg-[#1f2937] hover:text-white aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:border-[#6b7280] aria-disabled:hover:bg-white aria-disabled:hover:text-[#1f2937] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#c2410c] motion-reduce:transition-none" type="button" data-next aria-label="Next reviews"><svg viewBox="0 0 24 24" width="20" height="20" 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>
<ul class="flex gap-5 overflow-x-auto snap-x snap-mandatory overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:rounded-[20px] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#c2410c] px-0.5 pt-1 pb-[18px]" data-track tabindex="0" role="list" aria-label="Customer reviews, scroll sideways">
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Bakery · Porto</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>Saturday queues used to reach the corner. Now we take payment at the counter and on a tablet by the door, and the line moves twice as fast.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#c2410c] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">RC</span><span><span class="block font-bold text-[#1f2937]">Rita Carvalho</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Massa Mãe</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Bookshop · Leeds</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>Stock counts used to close the shop for a day each quarter. We now do them in the evening with two phones and a cup of tea.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#1d4ed8] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">OP</span><span><span class="block font-bold text-[#1f2937]">Owen Pritchard</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Manager, Second Chapter Books</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Florist · Utrecht</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>Wedding deposits, delivery slots and walk-in sales all land in one report. My accountant sent me a thank-you card.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#be123c] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">FW</span><span><span class="block font-bold text-[#1f2937]">Femke de Wit</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Founder, Wild Stems</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Bike shop · Denver</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>Repairs get a ticket, a text when they are ready, and a receipt that lists every part. Customers stopped calling to ask.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#15803d] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">MR</span><span><span class="block font-bold text-[#1f2937]">Marco Ruiz</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Co-owner, Spoke & Chain</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Café · Melbourne</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>I set up the menu, the tips and the kitchen printer myself on a Sunday night. Monday breakfast ran without a hiccup.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#7e22ce] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">GN</span><span><span class="block font-bold text-[#1f2937]">Grace Nguyen</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Little Ember</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Record store · Austin</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>Used vinyl is all one-offs. Scanning a sleeve and setting a price takes ten seconds, and the website updates on its own.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#0369a1] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">DB</span><span><span class="block font-bold text-[#1f2937]">Darnell Brooks</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Crate Diggers</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-[86%] snap-start md:basis-[calc((100%-20px)/2)] lg:basis-[calc((100%-40px)/3)]"><figure class="flex flex-1 flex-col gap-[18px] rounded-[20px] border border-[#e7e2da] bg-white p-[26px] shadow-[0_10px_24px_-18px_rgba(31,41,55,.35)]">
<span class="self-start rounded-full bg-[#fff1e6] px-2.5 py-1 text-[.78rem] font-bold text-[#9a3412]">Deli · Kraków</span>
<blockquote class="flex-1 text-base leading-[1.65] text-[#1f2937]"><p>We sell by weight, by slice and by the jar. It handles all three, and the scale talks to the till without a cable mess.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-[42px] rounded-[14px] flex-none place-items-center bg-[#a16207] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">ZN</span><span><span class="block font-bold text-[#1f2937]">Zofia Nowak</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Spiżarnia</span></span></figcaption>
</figure></li>
</ul>
</section></div>
<div><span class="mb-[18px] block text-[.72rem] tracking-[.12em] text-[#4b5563] uppercase">Split slider, one story at a time</span>
<section class="grid items-center gap-6 rounded-[28px] border border-[#fde4cc] bg-[#fff7ed] p-[clamp(24px,4vw,48px)] md:grid-cols-[minmax(0,5fr)_minmax(0,7fr)]" data-slider aria-labelledby="sl-b-title">
<div>
<p class="text-[.8rem] font-bold tracking-[.12em] text-[#c2410c] uppercase">Customer stories</p>
<h2 id="sl-b-title" class="mt-1.5 text-[clamp(1.5rem,1.1rem+1.5vw,2rem)] leading-[1.15] font-extrabold tracking-[-.02em] text-[#1f2937]">Stories from the shop floor</h2>
<p class="mt-3 text-[#4b5563]">Owners on what changed in their first month with Tillwise.</p>
<div class="mt-6 flex items-center gap-2.5" data-controls hidden>
<button class="grid size-11 place-items-center rounded-full border-[1.5px] border-[#6b7280] bg-white text-[#1f2937] cursor-pointer transition-colors duration-200 hover:border-[#1f2937] hover:bg-[#1f2937] hover:text-white aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:border-[#6b7280] aria-disabled:hover:bg-white aria-disabled:hover:text-[#1f2937] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#c2410c] motion-reduce:transition-none" type="button" data-prev aria-label="Previous story"><svg viewBox="0 0 24 24" width="20" height="20" 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 place-items-center rounded-full border-[1.5px] border-[#6b7280] bg-white text-[#1f2937] cursor-pointer transition-colors duration-200 hover:border-[#1f2937] hover:bg-[#1f2937] hover:text-white aria-disabled:cursor-default aria-disabled:opacity-40 aria-disabled:hover:border-[#6b7280] aria-disabled:hover:bg-white aria-disabled:hover:text-[#1f2937] focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-[#c2410c] motion-reduce:transition-none" type="button" data-next aria-label="Next story"><svg viewBox="0 0 24 24" width="20" height="20" 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>
<span class="min-w-[4.5ch] text-[.95rem] font-bold text-[#4b5563] tabular-nums" data-count>01 / 04</span>
</div>
</div>
<ul class="flex gap-5 overflow-x-auto snap-x snap-mandatory overscroll-x-contain [scrollbar-width:none] [&::-webkit-scrollbar]:hidden focus-visible:rounded-[20px] focus-visible:outline-3 focus-visible:outline-offset-4 focus-visible:outline-[#c2410c] -mb-4 px-0.5 pt-1 pb-5" data-track tabindex="0" role="list" aria-label="Customer stories, scroll sideways">
<li class="flex shrink-0 basis-full snap-start"><figure class="flex flex-1 flex-col gap-6 rounded-[20px] border border-[#fde4cc] bg-white p-[clamp(24px,3.5vw,40px)] shadow-[0_18px_40px_-26px_rgba(154,52,18,.45)]">
<blockquote class="flex-1 text-[clamp(1.1rem,1rem+.5vw,1.3rem)] leading-[1.55] font-medium text-[#1f2937] before:block before:h-[34px] before:font-[Georgia,'Times_New_Roman',serif] before:text-[3.6rem] before:leading-none before:font-bold before:text-[#c2410c] before:content-['“']"><p>The first week we switched, we found out which of our cakes actually sold. Two left the menu, and Saturday waste dropped by a third.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-12 rounded-full flex-none place-items-center bg-[#c2410c] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">HS</span><span><span class="block font-bold text-[#1f2937]">Helena Sørensen</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Kanel Bakehouse · customer since 2021</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-full snap-start"><figure class="flex flex-1 flex-col gap-6 rounded-[20px] border border-[#fde4cc] bg-white p-[clamp(24px,3.5vw,40px)] shadow-[0_18px_40px_-26px_rgba(154,52,18,.45)]">
<blockquote class="flex-1 text-[clamp(1.1rem,1rem+.5vw,1.3rem)] leading-[1.55] font-medium text-[#1f2937] before:block before:h-[34px] before:font-[Georgia,'Times_New_Roman',serif] before:text-[3.6rem] before:leading-none before:font-bold before:text-[#c2410c] before:content-['“']"><p>Three shops, one stock list. When the Leith branch runs out, the staff can see the other two still have it and send the customer over.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-12 rounded-full flex-none place-items-center bg-[#1d4ed8] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">CR</span><span><span class="block font-bold text-[#1f2937]">Callum Reid</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Director, Reid & Daughters Hardware</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-full snap-start"><figure class="flex flex-1 flex-col gap-6 rounded-[20px] border border-[#fde4cc] bg-white p-[clamp(24px,3.5vw,40px)] shadow-[0_18px_40px_-26px_rgba(154,52,18,.45)]">
<blockquote class="flex-1 text-[clamp(1.1rem,1rem+.5vw,1.3rem)] leading-[1.55] font-medium text-[#1f2937] before:block before:h-[34px] before:font-[Georgia,'Times_New_Roman',serif] before:text-[3.6rem] before:leading-none before:font-bold before:text-[#c2410c] before:content-['“']"><p>I was sure we would need an IT person. We needed a free afternoon and the setup guide, and that was it.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-12 rounded-full flex-none place-items-center bg-[#15803d] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">AB</span><span><span class="block font-bold text-[#1f2937]">Ama Boateng</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Kente Corner · customer since 2022</span></span></figcaption>
</figure></li>
<li class="flex shrink-0 basis-full snap-start"><figure class="flex flex-1 flex-col gap-6 rounded-[20px] border border-[#fde4cc] bg-white p-[clamp(24px,3.5vw,40px)] shadow-[0_18px_40px_-26px_rgba(154,52,18,.45)]">
<blockquote class="flex-1 text-[clamp(1.1rem,1rem+.5vw,1.3rem)] leading-[1.55] font-medium text-[#1f2937] before:block before:h-[34px] before:font-[Georgia,'Times_New_Roman',serif] before:text-[3.6rem] before:leading-none before:font-bold before:text-[#c2410c] before:content-['“']"><p>End of day used to mean counting coins at midnight. Now the till balances itself and I am home before the kids are asleep.</p></blockquote>
<figcaption class="flex items-center gap-3"><span class="grid size-12 rounded-full flex-none place-items-center bg-[#be123c] text-[.85rem] font-bold tracking-[.03em] text-white" aria-hidden="true">RC</span><span><span class="block font-bold text-[#1f2937]">Rafael Costa</span><span class="block text-[.875rem] leading-[1.4] text-[#4b5563]">Owner, Costa Gelato · customer since 2020</span></span></figcaption>
</figure></li>
</ul>
</section></div>
JavaScript
// Testimonial slider: the track is a CSS scroll-snap list that swipes, scrolls and takes arrow keys
// without any script. The script only reveals the buttons, moves one card per click, marks a button
// aria-disabled at either end and keeps the optional counter in step. Nothing moves on its own.
document.querySelectorAll('[data-slider]').forEach((root) => {
const track = root.querySelector('[data-track]');
const prev = root.querySelector('[data-prev]');
const next = root.querySelector('[data-next]');
const count = root.querySelector('[data-count]');
const slides = [...track.children];
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
const pad = (n) => String(n).padStart(2, '0');
const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
const sync = () => {
const end = track.scrollWidth - track.clientWidth - 2;
prev.setAttribute('aria-disabled', track.scrollLeft <= 2);
next.setAttribute('aria-disabled', track.scrollLeft >= end);
if (count) count.textContent = pad(Math.round(track.scrollLeft / step()) + 1) + ' / ' + pad(slides.length);
};
const go = (dir, btn) => {
if (btn.getAttribute('aria-disabled') === 'true') return;
track.scrollBy({ left: dir * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
};
prev.addEventListener('click', () => go(-1, prev));
next.addEventListener('click', () => go(1, next));
track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
window.addEventListener('resize', sync);
root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
sync();
});
About this set
Testimonial sliders that show several quotes at once and never move on their own. The card slider with arrow buttons shows three review cards on desktop, two on tablets and one with a peek of the next on phones, each with a shop type tag, the quote and an initials avatar, with round previous and next buttons beside the heading. The split slider puts the heading, an intro line, the buttons and a 01 / 04 counter in a left column and one large story at a time on the right. Both tracks are CSS scroll-snap lists, so they swipe on touch screens, scroll with a trackpad and take the arrow keys once focused, even if the script never loads. A few lines of vanilla JavaScript reveal the buttons, move exactly one card per click, mark a button aria-disabled at either end and keep the counter in step. There is no autoplay, and scrolling is instant when reduced motion is requested.
What’s included
Three-up card slider and a split one-story slider
CSS scroll-snap track that works without JavaScript
Buttons move one card per click and disable at the ends
No autoplay; instant scrolling under reduced motion
Same scroll-snap pattern in the Bootstrap and Tailwind versions
Accessibility
The track is a focusable list with an aria-label, so keyboard users can scroll it with the arrow keys and screen readers hear each quote as a list item. The previous and next buttons are named buttons that stay focusable and report aria-disabled at the ends, and the buttons, the track and the controls show a 3px orange focus outline. Smooth scrolling and button transitions stop under prefers-reduced-motion.
Customise it
Change --sl-accent, --sl-ink and --sl-line on .sl, and the flex-basis values on .sl-track > li to show more or fewer cards. Bootstrap uses the same variables plus --bs-btn-* on .sl-btn; in Tailwind edit the basis-[...] classes and the orange hex values.