Open demo

CSS code

HTML
<div><span class="lbl">Centred quote with arrows and dots</span>
<section class="tst" role="region" aria-roledescription="carousel" aria-label="Customer stories" data-carousel data-loop>
  <div class="tst-card">
    <div class="tst-track" data-track tabindex="0">
      <div class="tst-slide" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <span class="tst-stars" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote>“We moved forty freelancers onto Tally in a week. Invoices go out the day the work is signed off, and I no longer chase anyone for timesheets.”</blockquote>
        <div class="tst-person"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" width="56" height="56" alt=""><div><p class="tst-name">Hannah Lindqvist</p><p class="tst-role">Operations Lead, Northpoint Studio</p></div></div>
      </div>
      <div class="tst-slide" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <span class="tst-stars" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote>“I used to lose Sunday evenings to bookkeeping. Now the bank feed matches itself and I check it over coffee on Monday morning.”</blockquote>
        <div class="tst-person"><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" width="56" height="56" loading="lazy" alt=""><div><p class="tst-name">Marco Reyes</p><p class="tst-role">Founder, Tidewater Coffee</p></div></div>
      </div>
      <div class="tst-slide" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <span class="tst-stars" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote>“Our average payment time dropped from 38 days to 19. The automatic reminders are polite enough that clients thank us for them.”</blockquote>
        <div class="tst-person"><img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" width="56" height="56" loading="lazy" alt=""><div><p class="tst-name">Priya Raman</p><p class="tst-role">Finance Director, Larkspur Clinics</p></div></div>
      </div>
    </div>
    <button class="tst-arrow" type="button" data-prev data-controls hidden aria-label="Previous story"><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="tst-arrow" type="button" data-next data-controls hidden aria-label="Next story"><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 class="tst-dots" data-controls hidden>
    <button type="button" data-dot aria-label="Show story 1"></button>
    <button type="button" data-dot aria-label="Show story 2"></button>
    <button type="button" data-dot aria-label="Show story 3"></button>
  </div>
</section></div>

<div><span class="lbl">Reviews with avatar navigation</span>
<section class="rv" role="region" aria-roledescription="carousel" aria-label="Member reviews" data-carousel>
  <div class="rv-side">
    <div><p class="rv-score">4.8 <span>/ 5</span></p><p class="rv-text">Foundry Row members rate the space 4.8 out of 5 across 612 reviews.</p></div>
    <div class="rv-faces" data-controls hidden>
      <button type="button" data-dot aria-label="Show review from Amara Osei"><img src="https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
      <button type="button" data-dot aria-label="Show review from Jordan Ellis"><img src="https://images.unsplash.com/photo-1566492031773-4f4e44671857?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
      <button type="button" data-dot aria-label="Show review from Sofia Marin"><img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
      <button type="button" data-dot aria-label="Show review from Callum Reid"><img src="https://images.unsplash.com/photo-1552058544-f2b08422138a?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
    </div>
  </div>
  <div class="rv-track" data-track tabindex="0">
    <div class="rv-slide" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote>I came for the desk and stayed for the people. Three of my last five commissions came from members I met at the Thursday lunch.</blockquote>
      <p class="rv-name">Amara Osei</p><p class="rv-role">Illustrator, member since 2022</p>
    </div>
    <div class="rv-slide" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote>Fast wifi, phone booths that are actually quiet, and coffee someone clearly cares about. I get more done here in a morning than at home in a day.</blockquote>
      <p class="rv-name">Jordan Ellis</p><p class="rv-role">Backend developer, member since 2023</p>
    </div>
    <div class="rv-slide" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote>Meeting rooms book in two taps and reception signs for my parcels. Small things, but they add up to an office I don't have to run.</blockquote>
      <p class="rv-name">Sofia Marin</p><p class="rv-role">Architect, member since 2021</p>
    </div>
    <div class="rv-slide" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote>We grew from two desks to a private studio for eight without changing address, and nobody asked us to sign anything longer than a month.</blockquote>
      <p class="rv-name">Callum Reid</p><p class="rv-role">Founder, Brightside Bikes</p>
    </div>
  </div>
</section></div>
CSS
/* Testimonial sliders on a CSS scroll-snap track. The script reveals the arrows, dots and
   avatar buttons; without it the quotes still scroll and swipe. */
[data-controls][hidden]{display:none}
.tst{--tst-ink:#1e1b4b;--tst-muted:#55587a;--tst-accent:#4338ca;--tst-soft:#eceeff;--tst-dot:#7c7fa6;position:relative}
.tst-card{position:relative;border-radius:28px;background:#fff;box-shadow:0 1px 2px rgba(30,27,75,.06),0 18px 44px rgba(30,27,75,.08);overflow:hidden}
.tst-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.tst.is-ready .tst-track{scrollbar-width:none}
.tst-slide{flex:0 0 100%;scroll-snap-align:start;scroll-snap-stop:always;padding:56px 104px;text-align:center}
.tst-stars{display:inline-flex;gap:3px;color:#f59e0b}
.tst-stars svg{width:20px;height:20px}
.tst-slide blockquote{margin:22px auto 0;max-width:36em;font:italic 400 clamp(1.15rem,2vw,1.4rem)/1.55 Georgia,"Times New Roman",serif;color:var(--tst-ink)}
.tst-person{display:inline-flex;align-items:center;gap:14px;margin-top:28px;text-align:left}
.tst-person img{width:56px;height:56px;border-radius:50%;object-fit:cover}
.tst-name{font-weight:700;color:var(--tst-ink)}
.tst-role{font-size:.88rem;color:var(--tst-muted);margin-top:2px}
.tst-arrow{position:absolute;z-index:1;top:50%;margin-top:-22px;width:44px;height:44px;display:grid;place-items:center;border:0;border-radius:50%;
  background:var(--tst-soft);color:var(--tst-accent);cursor:pointer;transition:background .2s,color .2s}
.tst-arrow:hover{background:var(--tst-accent);color:#fff}
.tst-arrow[data-prev]{left:28px}.tst-arrow[data-next]{right:28px}
.tst-arrow svg{width:18px;height:18px}
.tst-dots{display:flex;justify-content:center;gap:4px;margin-top:20px}
.tst-dots button{width:24px;height:24px;display:grid;place-items:center;padding:0;border:0;background:none;cursor:pointer}
.tst-dots button::before{content:"";width:8px;height:8px;border-radius:4px;background:var(--tst-dot);transition:width .25s,background .25s}
.tst-dots button[aria-current=true]::before{width:22px;background:var(--tst-accent)}
.tst-track:focus-visible{outline:3px solid var(--tst-accent);outline-offset:-3px;border-radius:28px}
.rv-track:focus-visible{outline:3px solid var(--rv-accent);outline-offset:-3px}
.tst-arrow:focus-visible,.tst-dots button:focus-visible{outline:3px solid var(--tst-accent);outline-offset:2px}
@media (max-width:640px){
  .tst-slide{padding:34px 22px 88px}
  .tst-arrow{top:auto;bottom:22px;margin:0}.tst-arrow[data-prev]{left:22px}.tst-arrow[data-next]{right:22px}
  .tst-dots{position:relative;z-index:1;margin-top:-56px;margin-bottom:32px}}

/* Reviews with avatar navigation: the faces are the dots */
.rv{--rv-panel:#312e81;--rv-ink:#1e1b4b;--rv-muted:#55587a;--rv-accent:#4338ca;
  display:grid;grid-template-columns:minmax(260px,.8fr) 1.4fr;border-radius:28px;overflow:hidden;background:#fff;
  box-shadow:0 1px 2px rgba(30,27,75,.06),0 18px 44px rgba(30,27,75,.08)}
.rv-side{padding:40px 36px;background:var(--rv-panel);color:#fff;display:flex;flex-direction:column;justify-content:space-between;gap:28px}
.rv-score{font-size:3.2rem;font-weight:800;letter-spacing:-.03em;line-height:1}
.rv-score span{font-size:1.1rem;font-weight:600;color:#c7c9ff}
.rv-side .rv-text{margin-top:10px;font-size:.95rem;line-height:1.5;color:#e0e1ff}
.rv-faces{display:flex;flex-wrap:wrap;gap:10px}
.rv-faces button{width:52px;height:52px;padding:0;border:2px solid transparent;border-radius:50%;background:none;cursor:pointer;transition:border-color .2s,transform .2s}
.rv-faces img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;opacity:.72;transition:opacity .2s}
.rv-faces button:hover img,.rv-faces button[aria-current=true] img{opacity:1}
.rv-faces button[aria-current=true]{border-color:#fff;transform:scale(1.08)}
.rv-faces button:focus-visible{outline:3px solid #fbbf24;outline-offset:3px}
.rv-track{min-width:0;display:flex;overflow-x:auto;scroll-snap-type:x mandatory;overscroll-behavior-x:contain}
.rv.is-ready .rv-track{scrollbar-width:none}
.rv-slide{flex:0 0 100%;scroll-snap-align:start;scroll-snap-stop:always;padding:44px 48px;display:flex;flex-direction:column;justify-content:center}
.rv-mark{width:40px;height:40px;color:var(--rv-accent)}
.rv-slide blockquote{margin-top:16px;font-size:clamp(1.1rem,1.9vw,1.3rem);line-height:1.6;font-weight:500;color:var(--rv-ink)}
.rv-name{margin-top:22px;font-weight:700;color:var(--rv-ink)}
.rv-role{font-size:.88rem;color:var(--rv-muted);margin-top:2px}
@media (max-width:760px){.rv{grid-template-columns:1fr}.rv-side{padding:30px 24px}.rv-slide{padding:30px 24px 36px}}
@media (prefers-reduced-motion:reduce){.tst-arrow,.tst-dots button::before,.rv-faces button,.rv-faces img{transition:none}
  .rv-faces button[aria-current=true]{transform:none}}
JavaScript
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
  const track = root.querySelector('[data-track]');
  const slides = [...track.children];
  const dots = [...root.querySelectorAll('[data-dot]')];
  const prev = root.querySelector('[data-prev]');
  const next = root.querySelector('[data-next]');
  const loop = root.hasAttribute('data-loop');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
  const current = () => Math.round(track.scrollLeft / step());
  const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
  const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
  const move = (dir) => {
    if (loop && dir > 0 && atEnd()) return go(0);
    if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
    const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
    go(current() + dir * perView);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
    if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
    if (!loop && next) next.setAttribute('aria-disabled', atEnd());
  };
  prev?.addEventListener('click', () => move(-1));
  next?.addEventListener('click', () => move(1));
  dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
  track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div><span class="lbl">Centred quote with arrows and dots</span>
<div id="stories" class="carousel slide tst" role="region" aria-roledescription="carousel" aria-label="Customer stories">
  <div class="tst-card">
    <div class="carousel-inner" aria-live="polite">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <span class="tst-stars" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote>“We moved forty freelancers onto Tally in a week. Invoices go out the day the work is signed off, and I no longer chase anyone for timesheets.”</blockquote>
        <div class="tst-person"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" class="rounded-circle" width="56" height="56" alt=""><div><p class="tst-name">Hannah Lindqvist</p><p class="tst-role">Operations Lead, Northpoint Studio</p></div></div>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <span class="tst-stars" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote>“I used to lose Sunday evenings to bookkeeping. Now the bank feed matches itself and I check it over coffee on Monday morning.”</blockquote>
        <div class="tst-person"><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" class="rounded-circle" width="56" height="56" alt=""><div><p class="tst-name">Marco Reyes</p><p class="tst-role">Founder, Tidewater Coffee</p></div></div>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <span class="tst-stars" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote>“Our average payment time dropped from 38 days to 19. The automatic reminders are polite enough that clients thank us for them.”</blockquote>
        <div class="tst-person"><img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" class="rounded-circle" width="56" height="56" alt=""><div><p class="tst-name">Priya Raman</p><p class="tst-role">Finance Director, Larkspur Clinics</p></div></div>
      </div>
    </div>
    <button class="carousel-control-prev" type="button" data-bs-target="#stories" data-bs-slide="prev"><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><span class="visually-hidden">Previous story</span></button>
    <button class="carousel-control-next" type="button" data-bs-target="#stories" data-bs-slide="next"><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><span class="visually-hidden">Next story</span></button>
  </div>
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#stories" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show story 1"></button>
    <button type="button" data-bs-target="#stories" data-bs-slide-to="1" aria-label="Show story 2"></button>
    <button type="button" data-bs-target="#stories" data-bs-slide-to="2" aria-label="Show story 3"></button>
  </div>
</div></div>

<div><span class="lbl">Reviews with avatar navigation</span>
<div id="members" class="carousel slide carousel-fade rv" role="region" aria-roledescription="carousel" aria-label="Member reviews">
  <div class="row g-0 h-100">
    <div class="col-md-5 col-lg-4 rv-side">
      <div><p class="rv-score">4.8 <span>/ 5</span></p><p class="rv-text">Foundry Row members rate the space 4.8 out of 5 across 612 reviews.</p></div>
      <div class="carousel-indicators">
        <button type="button" data-bs-target="#members" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Show review from Amara Osei"><img src="https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
        <button type="button" data-bs-target="#members" data-bs-slide-to="1" aria-label="Show review from Jordan Ellis"><img src="https://images.unsplash.com/photo-1566492031773-4f4e44671857?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
        <button type="button" data-bs-target="#members" data-bs-slide-to="2" aria-label="Show review from Sofia Marin"><img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
        <button type="button" data-bs-target="#members" data-bs-slide-to="3" aria-label="Show review from Callum Reid"><img src="https://images.unsplash.com/photo-1552058544-f2b08422138a?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" width="48" height="48" alt=""></button>
      </div>
    </div>
    <div class="col-md-7 col-lg-8">
      <div class="carousel-inner" aria-live="polite">
        <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
          <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
          <blockquote>I came for the desk and stayed for the people. Three of my last five commissions came from members I met at the Thursday lunch.</blockquote>
          <p class="rv-name">Amara Osei</p><p class="rv-role">Illustrator, member since 2022</p>
        </div>
        <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
          <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
          <blockquote>Fast wifi, phone booths that are actually quiet, and coffee someone clearly cares about. I get more done here in a morning than at home in a day.</blockquote>
          <p class="rv-name">Jordan Ellis</p><p class="rv-role">Backend developer, member since 2023</p>
        </div>
        <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
          <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
          <blockquote>Meeting rooms book in two taps and reception signs for my parcels. Small things, but they add up to an office I don't have to run.</blockquote>
          <p class="rv-name">Sofia Marin</p><p class="rv-role">Architect, member since 2021</p>
        </div>
        <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
          <svg class="rv-mark" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
          <blockquote>We grew from two desks to a private studio for eight without changing address, and nobody asked us to sign anything longer than a month.</blockquote>
          <p class="rv-name">Callum Reid</p><p class="rv-role">Founder, Brightside Bikes</p>
        </div>
      </div>
    </div>
  </div>
</div></div>
CSS
/* Testimonial sliders on Bootstrap's .carousel: the first slides, the second cross-fades (.carousel-fade)
   and uses its indicators as avatar buttons. */
.tst{--tst-ink:#1e1b4b;--tst-muted:#55587a;--tst-accent:#4338ca;--tst-soft:#eceeff;--tst-dot:#7c7fa6}
.tst-card{position:relative;border-radius:28px;background:#fff;box-shadow:0 1px 2px rgba(30,27,75,.06),0 18px 44px rgba(30,27,75,.08);overflow:hidden}
.tst .carousel-item{padding:56px 104px;text-align:center}
.tst-stars{display:inline-flex;gap:3px;color:#f59e0b}
.tst-stars svg{width:20px;height:20px}
.tst blockquote{margin:22px auto 0;max-width:36em;font:italic 400 clamp(1.15rem,2vw,1.4rem)/1.55 Georgia,"Times New Roman",serif;color:var(--tst-ink)}
.tst-person{display:inline-flex;align-items:center;gap:14px;margin-top:28px;text-align:left}
.tst-person img{object-fit:cover}
.tst-name{margin:0;font-weight:700;color:var(--tst-ink)}
.tst-role{margin:2px 0 0;font-size:.88rem;color:var(--tst-muted)}
.tst .carousel-control-prev,.tst .carousel-control-next{top:50%;bottom:auto;width:44px;height:44px;margin-top:-22px;border-radius:50%;
  background:var(--tst-soft);color:var(--tst-accent);opacity:1;transition:background .2s,color .2s}
.tst .carousel-control-prev{left:28px}.tst .carousel-control-next{right:28px}
.tst .carousel-control-prev:hover,.tst .carousel-control-next:hover{background:var(--tst-accent);color:#fff}
.tst .carousel-control-prev svg,.tst .carousel-control-next svg{width:18px;height:18px}
.tst .carousel-indicators{position:static;margin:20px 0 0;gap:4px}
.tst .carousel-indicators [data-bs-target]{width:24px;height:24px;margin:0;display:grid;place-items:center;border:0;background:none;opacity:1;text-indent:0}
.tst .carousel-indicators [data-bs-target]::before{content:"";width:8px;height:8px;border-radius:4px;background:var(--tst-dot);transition:width .25s,background .25s}
.tst .carousel-indicators .active::before{width:22px;background:var(--tst-accent)}
.tst .carousel-control-prev:focus-visible,.tst .carousel-control-next:focus-visible,.tst .carousel-indicators [data-bs-target]:focus-visible{outline:3px solid var(--tst-accent);outline-offset:2px}
@media (max-width:640px){
  .tst .carousel-item{padding:34px 22px 88px}
  .tst .carousel-control-prev,.tst .carousel-control-next{top:auto;bottom:22px;margin:0}.tst .carousel-control-prev{left:22px}.tst .carousel-control-next{right:22px}
  .tst .carousel-indicators{position:relative;z-index:3;margin-top:-56px;margin-bottom:32px}}

/* Reviews with avatar navigation: the faces are the carousel indicators */
.rv{--rv-panel:#312e81;--rv-ink:#1e1b4b;--rv-muted:#55587a;--rv-accent:#4338ca;border-radius:28px;overflow:hidden;background:#fff;
  box-shadow:0 1px 2px rgba(30,27,75,.06),0 18px 44px rgba(30,27,75,.08)}
.rv-side{padding:40px 36px;background:var(--rv-panel);color:#fff;display:flex;flex-direction:column;justify-content:space-between;gap:28px}
.rv-score{margin:0;font-size:3.2rem;font-weight:800;letter-spacing:-.03em;line-height:1}
.rv-score span{font-size:1.1rem;font-weight:600;color:#c7c9ff}
.rv-text{margin:10px 0 0;font-size:.95rem;line-height:1.5;color:#e0e1ff}
.rv .carousel-indicators{position:static;margin:0;justify-content:flex-start;flex-wrap:wrap;gap:10px}
.rv .carousel-indicators [data-bs-target]{width:52px;height:52px;margin:0;padding:0;border:2px solid transparent;border-radius:50%;background:none;opacity:1;text-indent:0;
  transition:border-color .2s,transform .2s}
.rv .carousel-indicators img{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block;opacity:.72;transition:opacity .2s}
.rv .carousel-indicators [data-bs-target]:hover img,.rv .carousel-indicators .active img{opacity:1}
.rv .carousel-indicators .active{border-color:#fff;transform:scale(1.08)}
.rv .carousel-indicators [data-bs-target]:focus-visible{outline:3px solid #fbbf24;outline-offset:3px}
.rv .carousel-inner{height:100%}
.rv .carousel-item{height:100%;padding:44px 48px;flex-direction:column;justify-content:center}
.rv .carousel-item.active,.rv .carousel-item-next,.rv .carousel-item-prev{display:flex}
.rv-mark{width:40px;height:40px;color:var(--rv-accent)}
.rv blockquote{margin:16px 0 0;font-size:clamp(1.1rem,1.9vw,1.3rem);line-height:1.6;font-weight:500;color:var(--rv-ink)}
.rv-name{margin:22px 0 0;font-weight:700;color:var(--rv-ink)}
.rv-role{margin:2px 0 0;font-size:.88rem;color:var(--rv-muted)}
@media (max-width:767px){.rv-side{padding:30px 24px}.rv .carousel-item{padding:30px 24px 36px}}
@media (prefers-reduced-motion:reduce){.tst .carousel-control-prev,.tst .carousel-control-next,.tst .carousel-indicators [data-bs-target]::before,
  .rv .carousel-indicators [data-bs-target],.rv .carousel-indicators img{transition:none}.rv .carousel-indicators .active{transform:none}}
JavaScript
// Create the instances up front so the arrow keys work before the first click. No autoplay (ride is off).
document.querySelectorAll('.carousel').forEach((el) => bootstrap.Carousel.getOrCreateInstance(el));

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-tst-muted uppercase">Centred quote with arrows and dots</span>
<section class="relative" role="region" aria-roledescription="carousel" aria-label="Customer stories" data-carousel data-loop>
  <div class="relative overflow-hidden rounded-[28px] bg-white shadow-[0_1px_2px_rgba(30,27,75,.06),0_18px_44px_rgba(30,27,75,.08)]">
    <div class="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none] focus-visible:rounded-[28px] focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-tst-accent" data-track tabindex="0">
      <div class="shrink-0 basis-full snap-start snap-always px-[22px] pt-[34px] pb-[88px] text-center sm:px-[104px] sm:py-14" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <span class="inline-flex gap-[3px] text-[#f59e0b] *:size-5" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote class="mx-auto mt-[22px] max-w-[36em] font-[Georgia,'Times_New_Roman',serif] text-[clamp(1.15rem,2vw,1.4rem)] leading-[1.55] text-tst-ink italic">“We moved forty freelancers onto Tally in a week. Invoices go out the day the work is signed off, and I no longer chase anyone for timesheets.”</blockquote>
        <div class="mt-7 inline-flex items-center gap-3.5 text-left"><img src="https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" class="size-14 rounded-full object-cover" width="56" height="56" alt=""><div><p class="font-bold text-tst-ink">Hannah Lindqvist</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Operations Lead, Northpoint Studio</p></div></div>
      </div>
      <div class="shrink-0 basis-full snap-start snap-always px-[22px] pt-[34px] pb-[88px] text-center sm:px-[104px] sm:py-14" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <span class="inline-flex gap-[3px] text-[#f59e0b] *:size-5" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote class="mx-auto mt-[22px] max-w-[36em] font-[Georgia,'Times_New_Roman',serif] text-[clamp(1.15rem,2vw,1.4rem)] leading-[1.55] text-tst-ink italic">“I used to lose Sunday evenings to bookkeeping. Now the bank feed matches itself and I check it over coffee on Monday morning.”</blockquote>
        <div class="mt-7 inline-flex items-center gap-3.5 text-left"><img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" class="size-14 rounded-full object-cover" width="56" height="56" loading="lazy" alt=""><div><p class="font-bold text-tst-ink">Marco Reyes</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Founder, Tidewater Coffee</p></div></div>
      </div>
      <div class="shrink-0 basis-full snap-start snap-always px-[22px] pt-[34px] pb-[88px] text-center sm:px-[104px] sm:py-14" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <span class="inline-flex gap-[3px] text-[#f59e0b] *:size-5" role="img" aria-label="Rated 5 out of 5"><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg><svg viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path d="M10 1.5l2.6 5.4 5.9.8-4.3 4.1 1 5.9L10 14.9l-5.2 2.8 1-5.9L1.5 7.7l5.9-.8z"/></svg></span>
        <blockquote class="mx-auto mt-[22px] max-w-[36em] font-[Georgia,'Times_New_Roman',serif] text-[clamp(1.15rem,2vw,1.4rem)] leading-[1.55] text-tst-ink italic">“Our average payment time dropped from 38 days to 19. The automatic reminders are polite enough that clients thank us for them.”</blockquote>
        <div class="mt-7 inline-flex items-center gap-3.5 text-left"><img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&crop=faces&w=160&h=160&q=70" class="size-14 rounded-full object-cover" width="56" height="56" loading="lazy" alt=""><div><p class="font-bold text-tst-ink">Priya Raman</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Finance Director, Larkspur Clinics</p></div></div>
      </div>
    </div>
    <button class="absolute top-1/2 z-[1] -mt-[22px] grid size-11 cursor-pointer place-items-center rounded-full bg-tst-soft text-tst-accent transition-[background-color,color] duration-200 hover:bg-tst-accent hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tst-accent motion-reduce:transition-none max-sm:top-auto max-sm:bottom-[22px] max-sm:mt-0 left-7 max-sm:left-[22px]" type="button" data-prev data-controls hidden aria-label="Previous story"><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-1/2 z-[1] -mt-[22px] grid size-11 cursor-pointer place-items-center rounded-full bg-tst-soft text-tst-accent transition-[background-color,color] duration-200 hover:bg-tst-accent hover:text-white focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tst-accent motion-reduce:transition-none max-sm:top-auto max-sm:bottom-[22px] max-sm:mt-0 right-7 max-sm:right-[22px]" type="button" data-next data-controls hidden aria-label="Next story"><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 class="mt-5 flex justify-center gap-1 max-sm:relative max-sm:z-[1] max-sm:mt-[-56px] max-sm:mb-8" data-controls hidden>
    <button class="grid size-6 cursor-pointer place-items-center p-0 before:h-2 before:w-2 before:rounded-full before:bg-tst-dot before:transition-[width,background-color] before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tst-accent aria-[current=true]:before:w-[22px] aria-[current=true]:before:bg-tst-accent motion-reduce:before:transition-none" type="button" data-dot aria-label="Show story 1"></button>
    <button class="grid size-6 cursor-pointer place-items-center p-0 before:h-2 before:w-2 before:rounded-full before:bg-tst-dot before:transition-[width,background-color] before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tst-accent aria-[current=true]:before:w-[22px] aria-[current=true]:before:bg-tst-accent motion-reduce:before:transition-none" type="button" data-dot aria-label="Show story 2"></button>
    <button class="grid size-6 cursor-pointer place-items-center p-0 before:h-2 before:w-2 before:rounded-full before:bg-tst-dot before:transition-[width,background-color] before:duration-250 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-tst-accent aria-[current=true]:before:w-[22px] aria-[current=true]:before:bg-tst-accent motion-reduce:before:transition-none" type="button" data-dot aria-label="Show story 3"></button>
  </div>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-tst-muted uppercase">Reviews with avatar navigation</span>
<section class="grid overflow-hidden rounded-[28px] bg-white shadow-[0_1px_2px_rgba(30,27,75,.06),0_18px_44px_rgba(30,27,75,.08)] md:grid-cols-[minmax(260px,.8fr)_1.4fr]" role="region" aria-roledescription="carousel" aria-label="Member reviews" data-carousel>
  <div class="flex flex-col justify-between gap-7 bg-rv-panel px-6 py-[30px] text-white md:px-9 md:py-10">
    <div><p class="text-[3.2rem] leading-none font-extrabold tracking-[-.03em]">4.8 <span class="text-[1.1rem] font-semibold text-[#c7c9ff]">/ 5</span></p><p class="mt-2.5 text-[.95rem] leading-normal text-[#e0e1ff]">Foundry Row members rate the space 4.8 out of 5 across 612 reviews.</p></div>
    <div class="flex flex-wrap gap-2.5" data-controls hidden>
      <button class="group/face size-[52px] cursor-pointer rounded-full border-2 border-transparent p-0 transition-[border-color,transform] duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#fbbf24] aria-[current=true]:scale-108 aria-[current=true]:border-white motion-reduce:transition-none motion-reduce:aria-[current=true]:scale-100" type="button" data-dot aria-label="Show review from Amara Osei"><img src="https://images.unsplash.com/photo-1531123897727-8f129e1688ce?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" class="block size-full rounded-full object-cover opacity-72 transition-opacity duration-200 group-hover/face:opacity-100 group-aria-[current=true]/face:opacity-100 motion-reduce:transition-none" width="48" height="48" alt=""></button>
      <button class="group/face size-[52px] cursor-pointer rounded-full border-2 border-transparent p-0 transition-[border-color,transform] duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#fbbf24] aria-[current=true]:scale-108 aria-[current=true]:border-white motion-reduce:transition-none motion-reduce:aria-[current=true]:scale-100" type="button" data-dot aria-label="Show review from Jordan Ellis"><img src="https://images.unsplash.com/photo-1566492031773-4f4e44671857?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" class="block size-full rounded-full object-cover opacity-72 transition-opacity duration-200 group-hover/face:opacity-100 group-aria-[current=true]/face:opacity-100 motion-reduce:transition-none" width="48" height="48" alt=""></button>
      <button class="group/face size-[52px] cursor-pointer rounded-full border-2 border-transparent p-0 transition-[border-color,transform] duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#fbbf24] aria-[current=true]:scale-108 aria-[current=true]:border-white motion-reduce:transition-none motion-reduce:aria-[current=true]:scale-100" type="button" data-dot aria-label="Show review from Sofia Marin"><img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" class="block size-full rounded-full object-cover opacity-72 transition-opacity duration-200 group-hover/face:opacity-100 group-aria-[current=true]/face:opacity-100 motion-reduce:transition-none" width="48" height="48" alt=""></button>
      <button class="group/face size-[52px] cursor-pointer rounded-full border-2 border-transparent p-0 transition-[border-color,transform] duration-200 focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#fbbf24] aria-[current=true]:scale-108 aria-[current=true]:border-white motion-reduce:transition-none motion-reduce:aria-[current=true]:scale-100" type="button" data-dot aria-label="Show review from Callum Reid"><img src="https://images.unsplash.com/photo-1552058544-f2b08422138a?auto=format&fit=crop&crop=faces&w=120&h=120&q=70" class="block size-full rounded-full object-cover opacity-72 transition-opacity duration-200 group-hover/face:opacity-100 group-aria-[current=true]/face:opacity-100 motion-reduce:transition-none" width="48" height="48" alt=""></button>
    </div>
  </div>
  <div class="flex min-w-0 snap-x snap-mandatory overflow-x-auto overscroll-x-contain in-[.is-ready]:[scrollbar-width:none] focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-rv-accent" data-track tabindex="0">
    <div class="flex shrink-0 basis-full snap-start snap-always flex-col justify-center px-6 pt-[30px] pb-9 md:px-12 md:py-11" role="group" aria-roledescription="slide" aria-label="1 of 4">
      <svg class="size-10 text-rv-accent" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote class="mt-4 text-[clamp(1.1rem,1.9vw,1.3rem)] leading-[1.6] font-medium text-tst-ink">I came for the desk and stayed for the people. Three of my last five commissions came from members I met at the Thursday lunch.</blockquote>
      <p class="mt-[22px] font-bold text-tst-ink">Amara Osei</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Illustrator, member since 2022</p>
    </div>
    <div class="flex shrink-0 basis-full snap-start snap-always flex-col justify-center px-6 pt-[30px] pb-9 md:px-12 md:py-11" role="group" aria-roledescription="slide" aria-label="2 of 4">
      <svg class="size-10 text-rv-accent" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote class="mt-4 text-[clamp(1.1rem,1.9vw,1.3rem)] leading-[1.6] font-medium text-tst-ink">Fast wifi, phone booths that are actually quiet, and coffee someone clearly cares about. I get more done here in a morning than at home in a day.</blockquote>
      <p class="mt-[22px] font-bold text-tst-ink">Jordan Ellis</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Backend developer, member since 2023</p>
    </div>
    <div class="flex shrink-0 basis-full snap-start snap-always flex-col justify-center px-6 pt-[30px] pb-9 md:px-12 md:py-11" role="group" aria-roledescription="slide" aria-label="3 of 4">
      <svg class="size-10 text-rv-accent" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote class="mt-4 text-[clamp(1.1rem,1.9vw,1.3rem)] leading-[1.6] font-medium text-tst-ink">Meeting rooms book in two taps and reception signs for my parcels. Small things, but they add up to an office I don't have to run.</blockquote>
      <p class="mt-[22px] font-bold text-tst-ink">Sofia Marin</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Architect, member since 2021</p>
    </div>
    <div class="flex shrink-0 basis-full snap-start snap-always flex-col justify-center px-6 pt-[30px] pb-9 md:px-12 md:py-11" role="group" aria-roledescription="slide" aria-label="4 of 4">
      <svg class="size-10 text-rv-accent" viewBox="0 0 40 40" fill="currentColor" aria-hidden="true"><path d="M17 10c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7zm16 0c-6 1.6-10 6.4-10 13.2V30h10v-9.6h-5.4c.3-3.6 2.6-6 6.4-7z"/></svg>
      <blockquote class="mt-4 text-[clamp(1.1rem,1.9vw,1.3rem)] leading-[1.6] font-medium text-tst-ink">We grew from two desks to a private studio for eight without changing address, and nobody asked us to sign anything longer than a month.</blockquote>
      <p class="mt-[22px] font-bold text-tst-ink">Callum Reid</p><p class="mt-0.5 text-[.88rem] text-tst-muted">Founder, Brightside Bikes</p>
    </div>
  </div>
</section></div>
CSS
@theme {
  --color-tst-ink: #1e1b4b;
  --color-tst-muted: #55587a;
  --color-tst-accent: #4338ca;
  --color-tst-soft: #eceeff;
  --color-tst-dot: #7c7fa6;
  --color-rv-panel: #312e81;
  --color-rv-accent: #4338ca;
}
JavaScript
// Scroll-snap carousel. The track scrolls and swipes without this script; it only reveals
// the buttons and dots, wires them up and keeps their state in sync with the scroll position.
document.querySelectorAll('[data-carousel]').forEach((root) => {
  const track = root.querySelector('[data-track]');
  const slides = [...track.children];
  const dots = [...root.querySelectorAll('[data-dot]')];
  const prev = root.querySelector('[data-prev]');
  const next = root.querySelector('[data-next]');
  const loop = root.hasAttribute('data-loop');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)');
  const step = () => (slides[1] ? slides[1].offsetLeft - slides[0].offsetLeft : track.clientWidth);
  const current = () => Math.round(track.scrollLeft / step());
  const atEnd = () => track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
  const go = (i) => track.scrollTo({ left: i * step(), behavior: reduce.matches ? 'auto' : 'smooth' });
  const move = (dir) => {
    if (loop && dir > 0 && atEnd()) return go(0);
    if (loop && dir < 0 && track.scrollLeft < 2) return go(slides.length - 1);
    const perView = Math.max(1, Math.floor((track.clientWidth + step() - slides[0].offsetWidth + 2) / step()));
    go(current() + dir * perView);
  };
  const sync = () => {
    const i = current();
    dots.forEach((dot, n) => dot.setAttribute('aria-current', n === i));
    if (!loop && prev) prev.setAttribute('aria-disabled', track.scrollLeft < 2);
    if (!loop && next) next.setAttribute('aria-disabled', atEnd());
  };
  prev?.addEventListener('click', () => move(-1));
  next?.addEventListener('click', () => move(1));
  dots.forEach((dot, n) => dot.addEventListener('click', () => go(n)));
  track.addEventListener('scroll', () => requestAnimationFrame(sync), { passive: true });
  root.querySelectorAll('[data-controls]').forEach((el) => { el.hidden = false; });
  root.classList.add('is-ready');
  sync();
});

About this set

Two ways to rotate customer quotes. Centred quote with arrows and dots is a white card with five stars, an italic serif quote, and the customer’s photo, name and role, with soft round arrows at the sides and dots underneath; on phones the arrows drop to the bottom of the card with the dots between them. Reviews with avatar navigation pairs an indigo panel holding the overall score, 4.8 out of 5, with a white panel for the quote, and the reviewers’ faces are the navigation: pick a face to read that review. Use the centred card on a landing page and the avatar version where you want to show that many different people said something. In the plain CSS and Tailwind versions the quotes sit on a scroll-snap track that swipes without JavaScript, and a short script reveals and wires the arrows, dots and faces. The Bootstrap version uses .carousel for the first and .carousel-fade for the second, with the indicators restyled as dots and as avatar buttons, so Bootstrap keeps the active state in sync.

What’s included

  • Centred quote card with stars, photo, name and role
  • Avatar buttons that act as the carousel's dots
  • Star rating exposed as one image labelled "Rated 5 out of 5"
  • Quote track is keyboard scrollable because the slides contain no links
  • Bootstrap versions use .carousel and .carousel-fade

Accessibility

Both sliders are labelled regions with aria-roledescription="carousel" and slides announced as "2 of 3". The dots are named "Show story 2" and the faces "Show review from Sofia Marin", with aria-current on the one showing; the photos inside them have empty alt text so the name is read once. Buttons and the scrollable track show a 3px outline on keyboard focus, nothing moves on its own, and transitions are removed under prefers-reduced-motion.

Customise it

Change --tst-accent, --tst-soft and --tst-dot on .tst, and --rv-panel and --rv-accent on .rv. In Tailwind these are the --color-tst-* and --color-rv-* values in @theme.