// 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>
// 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.