Bootstrap Testimonial Slider: 16 Examples to Copy

A Bootstrap testimonial slider runs on the framework's own carousel plugin, so no separate slider library is needed. One catch up front: none of the six carousel options in Bootstrap 5.3 (interval, keyboard, pause, ride, touch and wrap) sets how many testimonials show at once.

Below are 16 Bootstrap testimonial slider examples you can copy, followed by a guide to markup, autoplay, multi-card layouts and accessibility.

By Bogdan Sandu · Updated October 29, 2026

Every example runs on Bootstrap 5.3.8 from the jsDelivr CDN. Add these tags to your page, then paste an example's HTML, CSS and JS.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.2/font/bootstrap-icons.min.css" rel="stylesheet" integrity="sha384-nWN3KqaaVcbEsCjLSV+2jMjh62Iny/+mN3srM8TZeM8N6aj7eDl5wwYggjAJ4/sJ" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>

One quote at a time

Classic testimonial sliders that show a single quote per slide: avatar indicators, a crossfade, star ratings, a pull quote with hard numbers and a split landing-page section. Every slide is a figure with a blockquote and a figcaption, so the attribution stays outside the quote.

Centered quote with avatar indicators

  • carousel-indicators
  • inline SVG avatars
  • aria-current
  • figure + blockquote

Use the arrows or the indicators. On a touch screen you can also swipe.

The indicator buttons are restyled into round initials avatars: the scoped CSS resets the 30px by 3px bar (text-indent, height, border, background) and Bootstrap still moves .active and aria-current for you. Each button keeps an aria-label with the customer name, and the SVG inside is aria-hidden. A min-height on the slides keeps the card from jumping between quotes.

View code
<div class="bts-ini">
  <div id="bts-ini" class="carousel slide" aria-roledescription="carousel" aria-label="What Plotwise customers say">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <figure class="bts-ini-fig">
          <i class="bi bi-quote bts-ini-mark" aria-hidden="true"></i>
          <blockquote class="blockquote bts-ini-quote">
            <p>We dropped the Monday status meeting. Everyone opens the board, sees what moved, and we spend that hour shipping instead.</p>
          </blockquote>
          <figcaption class="bts-ini-cap"><strong>Mara Okafor</strong><span>Head of Delivery, Fieldnote Studio</span></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <figure class="bts-ini-fig">
          <i class="bi bi-quote bts-ini-mark" aria-hidden="true"></i>
          <blockquote class="blockquote bts-ini-quote">
            <p>Client approvals used to live in six email threads. Now they sit next to the task, with the file and the date.</p>
          </blockquote>
          <figcaption class="bts-ini-cap"><strong>Tomas Rivera</strong><span>Producer, Brightwater Labs</span></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <figure class="bts-ini-fig">
          <i class="bi bi-quote bts-ini-mark" aria-hidden="true"></i>
          <blockquote class="blockquote bts-ini-quote">
            <p>The timeline view finally showed us why the spring catalog was late. We fixed the handoff, not the people.</p>
          </blockquote>
          <figcaption class="bts-ini-cap"><strong>Hana Sato</strong><span>Operations, Kiln and Clay Ceramics</span></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <figure class="bts-ini-fig">
          <i class="bi bi-quote bts-ini-mark" aria-hidden="true"></i>
          <blockquote class="blockquote bts-ini-quote">
            <p>Forty drivers, three depots, one board. Setup took an afternoon and nobody asked for training twice.</p>
          </blockquote>
          <figcaption class="bts-ini-cap"><strong>Elliot Brandt</strong><span>Fleet lead, Northline Freight</span></figcaption>
        </figure>
      </div>
    </div>
    <div class="carousel-indicators">
      <button type="button" data-bs-target="#bts-ini" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Quote from Mara Okafor"><svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#0f766e"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">MO</text></svg></button>
      <button type="button" data-bs-target="#bts-ini" data-bs-slide-to="1" aria-label="Quote from Tomas Rivera"><svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#1d4ed8"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">TR</text></svg></button>
      <button type="button" data-bs-target="#bts-ini" data-bs-slide-to="2" aria-label="Quote from Hana Sato"><svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#9a3412"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">HS</text></svg></button>
      <button type="button" data-bs-target="#bts-ini" data-bs-slide-to="3" aria-label="Quote from Elliot Brandt"><svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#6d28d9"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">EB</text></svg></button>
    </div>
  </div>
</div>

Editorial crossfade with a slide counter

  • carousel-fade
  • slid.bs.carousel
  • --bs-btn-*
  • <cite>

Use the arrows or the indicators. On a touch screen you can also swipe.

Adding carousel-fade swaps the slide for a crossfade, which reads calmer for text-only quotes. Text slides need their own background or the outgoing quote shows through, so each carousel-item gets the card color. The counter updates from the slid.bs.carousel event, and the outline buttons are themed only through --bs-btn-* variables.

View code
<div class="bts-fade">
  <div class="d-flex align-items-baseline justify-content-between gap-3 mb-3">
    <h2 class="bts-fade-title">Guests on The Marlow Quay</h2>
    <p class="bts-fade-count mb-0" aria-hidden="true"><span class="bts-fade-now">1</span> / 4</p>
  </div>
  <div id="bts-fade" class="carousel slide carousel-fade" aria-roledescription="carousel" aria-label="Guest reviews">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <figure class="mb-0">
          <blockquote class="bts-fade-quote"><p>The room looked straight onto the harbor, and someone had left a hand-drawn map of the best bakeries on the desk. We used it every morning.</p></blockquote>
          <figcaption class="bts-fade-cap">Ines Albrecht, <cite>three nights in October</cite></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <figure class="mb-0">
          <blockquote class="bts-fade-quote"><p>Quiet, warm and properly dark at night. I slept better here than at home, and breakfast ran until eleven.</p></blockquote>
          <figcaption class="bts-fade-cap">Callum Reid, <cite>business trip, one night</cite></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <figure class="mb-0">
          <blockquote class="bts-fade-quote"><p>Our train was cancelled and the front desk rebooked us, found a later checkout and lent us an umbrella. Small things, all of them right.</p></blockquote>
          <figcaption class="bts-fade-cap">Noor and Sam Haddad, <cite>weekend in September</cite></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <figure class="mb-0">
          <blockquote class="bts-fade-quote"><p>Book the corner room on the third floor. The light at seven in the morning is worth the stairs.</p></blockquote>
          <figcaption class="bts-fade-cap">Odile Marchand, <cite>fourth stay this year</cite></figcaption>
        </figure>
      </div>
    </div>
  </div>
  <div class="d-flex gap-2 mt-4">
    <button type="button" class="btn bts-fade-btn" data-bs-target="#bts-fade" data-bs-slide="prev"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="ms-2">Previous</span><span class="visually-hidden"> review</span></button>
    <button type="button" class="btn bts-fade-btn" data-bs-target="#bts-fade" data-bs-slide="next"><span class="me-2">Next</span><span class="visually-hidden"> review</span><i class="bi bi-arrow-right" aria-hidden="true"></i></button>
  </div>
</div>

Product reviews with star ratings

  • Bootstrap Icons
  • visually-hidden
  • --bs-carousel-control-icon-filter
  • --bs-carousel-indicator-active-bg

Use the arrows or the indicators. On a touch screen you can also swipe.

Stars come from Bootstrap Icons and are aria-hidden, while a visually-hidden line says Rated 4.5 out of 5, so the score survives without the visuals. The default arrows and dots are made dark on a white card through the two carousel CSS variables instead of data-bs-theme, which would flip every color inside. The control width is narrowed so the arrows sit in the side padding.

View code
<div class="bts-star">
  <div class="bts-star-top d-flex flex-wrap align-items-center justify-content-between gap-2">
    <h2 class="bts-star-title">Reviews of the Arc pour-over kettle</h2>
    <span class="badge rounded-pill bts-star-sum">4.7 average</span>
  </div>
  <div id="bts-star" class="carousel slide" aria-roledescription="carousel" aria-label="Customer reviews">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <figure class="bts-star-fig">
          <p class="bts-star-rating mb-2"><span class="bts-star-icons" aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i></span><span class="visually-hidden">Rated 5 out of 5</span><span class="bts-star-num" aria-hidden="true">5.0</span></p>
          <p class="bts-star-head">Pours like it was made for one job</p>
          <blockquote class="bts-star-quote"><p>The spout gives a thin, steady stream even when the kettle is full. The handle stays cool and the lid does not rattle.</p></blockquote>
          <figcaption class="bts-star-cap">Dara Quinn <span class="badge text-bg-light border bts-star-badge"><i class="bi bi-patch-check-fill" aria-hidden="true"></i> Verified buyer</span></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <figure class="bts-star-fig">
          <p class="bts-star-rating mb-2"><span class="bts-star-icons" aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-half"></i></span><span class="visually-hidden">Rated 4.5 out of 5</span><span class="bts-star-num" aria-hidden="true">4.5</span></p>
          <p class="bts-star-head">Great on gas, fine on induction</p>
          <blockquote class="bts-star-quote"><p>Heats fast on my gas hob. On the induction plate at work it needs the larger ring, which the listing does mention.</p></blockquote>
          <figcaption class="bts-star-cap">Felix Moreau <span class="badge text-bg-light border bts-star-badge"><i class="bi bi-patch-check-fill" aria-hidden="true"></i> Verified buyer</span></figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <figure class="bts-star-fig">
          <p class="bts-star-rating mb-2"><span class="bts-star-icons" aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star"></i></span><span class="visually-hidden">Rated 4 out of 5</span><span class="bts-star-num" aria-hidden="true">4.0</span></p>
          <p class="bts-star-head">Lovely, but check the capacity</p>
          <blockquote class="bts-star-quote"><p>It holds 0.9 liters, enough for two big mugs. Beautiful matte finish, and the thermometer in the lid is accurate.</p></blockquote>
          <figcaption class="bts-star-cap">Grace Lindqvist</figcaption>
        </figure>
      </div>
    </div>
    <button class="carousel-control-prev" type="button" data-bs-target="#bts-star" data-bs-slide="prev"><span class="carousel-control-prev-icon" aria-hidden="true"></span><span class="visually-hidden">Previous review</span></button>
    <button class="carousel-control-next" type="button" data-bs-target="#bts-star" data-bs-slide="next"><span class="carousel-control-next-icon" aria-hidden="true"></span><span class="visually-hidden">Next review</span></button>
    <div class="carousel-indicators">
      <button type="button" data-bs-target="#bts-star" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Review 1"></button>
      <button type="button" data-bs-target="#bts-star" data-bs-slide-to="1" aria-label="Review 2"></button>
      <button type="button" data-bs-target="#bts-star" data-bs-slide-to="2" aria-label="Review 3"></button>
    </div>
  </div>
</div>

Pull quote with company metrics

  • row + col-md-*
  • description list
  • data-bs-slide
  • object metrics

Use the arrows or the indicators. On a touch screen you can also swipe.

A case-study slider: each carousel-item holds a grid row with a big pull quote on the left and three results in a description list on the right, stacked under md. The previous and next buttons live in the header, outside the carousel, and still work because data-bs-target points at the carousel id.

View code
<div class="bts-case">
  <div class="d-flex align-items-center justify-content-between gap-3 mb-4">
    <h2 class="bts-case-title">Results from Routekeep customers</h2>
    <div class="d-flex gap-2 flex-shrink-0">
      <button type="button" class="btn bts-case-nav" data-bs-target="#bts-case" data-bs-slide="prev"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="visually-hidden">Previous case study</span></button>
      <button type="button" class="btn bts-case-nav" data-bs-target="#bts-case" data-bs-slide="next"><i class="bi bi-arrow-right" aria-hidden="true"></i><span class="visually-hidden">Next case study</span></button>
    </div>
  </div>
  <div id="bts-case" class="carousel slide" aria-roledescription="carousel" aria-label="Customer case studies">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <div class="row g-4">
          <div class="col-md-7">
            <figure class="mb-0">
              <blockquote class="bts-case-quote"><p>We cut failed deliveries by a third in one quarter, without hiring a single dispatcher.</p></blockquote>
              <figcaption class="d-flex align-items-center gap-3">
                <svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#14532d"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">RA</text></svg>
                <span><strong class="d-block">Ruth Adeyemi</strong><span class="bts-case-role">COO, Harbor and Pine Logistics</span></span>
              </figcaption>
            </figure>
          </div>
          <div class="col-md-5">
            <dl class="bts-case-stats">
              <div><dt>Failed deliveries</dt><dd>&minus;34%</dd></div>
              <div><dt>Daily route planning</dt><dd>20 min</dd></div>
              <div><dt>Drivers onboarded</dt><dd>212</dd></div>
            </dl>
            <a href="#" class="bts-case-link">Read the Harbor and Pine story <i class="bi bi-arrow-right" aria-hidden="true"></i></a>
          </div>
        </div>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <div class="row g-4">
          <div class="col-md-7">
            <figure class="mb-0">
              <blockquote class="bts-case-quote"><p>Customers get a live arrival window now, so our call center stopped answering where is my parcel.</p></blockquote>
              <figcaption class="d-flex align-items-center gap-3">
                <svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#1e3a8a"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">JK</text></svg>
                <span><strong class="d-block">Jonas Keller</strong><span class="bts-case-role">Head of Service, Velo Grocer</span></span>
              </figcaption>
            </figure>
          </div>
          <div class="col-md-5">
            <dl class="bts-case-stats">
              <div><dt>Support calls</dt><dd>&minus;52%</dd></div>
              <div><dt>On-time rate</dt><dd>96.8%</dd></div>
              <div><dt>Weekly orders</dt><dd>18,400</dd></div>
            </dl>
            <a href="#" class="bts-case-link">Read the Velo Grocer story <i class="bi bi-arrow-right" aria-hidden="true"></i></a>
          </div>
        </div>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <div class="row g-4">
          <div class="col-md-7">
            <figure class="mb-0">
              <blockquote class="bts-case-quote"><p>Two vans fewer on the road, same number of stops. The fuel savings paid for the rollout by March.</p></blockquote>
              <figcaption class="d-flex align-items-center gap-3">
                <svg viewBox="0 0 48 48" width="48" height="48" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#9a3412"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">LM</text></svg>
                <span><strong class="d-block">Lucia Marin</strong><span class="bts-case-role">Owner, Marin Florists</span></span>
              </figcaption>
            </figure>
          </div>
          <div class="col-md-5">
            <dl class="bts-case-stats">
              <div><dt>Vans needed</dt><dd>6 &rarr; 4</dd></div>
              <div><dt>Fuel per stop</dt><dd>&minus;27%</dd></div>
              <div><dt>Payback</dt><dd>4 months</dd></div>
            </dl>
            <a href="#" class="bts-case-link">Read the Marin Florists story <i class="bi bi-arrow-right" aria-hidden="true"></i></a>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Landing page section with a quote card slider

  • container + row
  • carousel-indicators
  • --bs-btn-*
  • data-bs-theme

Use the arrows or the indicators. On a touch screen you can also swipe.

A full-width section where the pitch and call to action sit in one column and the slider sits in a white card in the other. The section uses data-bs-theme="dark" for its own text, the card sets data-bs-theme="light" back, and the indicators become short bars below the quote with position: static. The lime button is a .btn themed only with --bs-btn-* variables.

View code
<div class="bts-split" data-bs-theme="dark">
  <div class="container">
    <div class="row g-4 g-lg-5 align-items-center">
      <div class="col-md-5">
        <h2 class="bts-split-title">Teams switch to Quillbase for the search</h2>
        <p class="bts-split-lead">Your docs, tickets and meeting notes in one place, with answers that cite the page they came from.</p>
        <div class="d-flex flex-wrap gap-2">
          <a href="#" class="btn bts-split-cta">Start a free trial</a>
          <a href="#" class="btn btn-outline-light bts-split-ghost">Book a demo</a>
        </div>
      </div>
      <div class="col-md-7">
        <div class="bts-split-card" data-bs-theme="light">
          <div id="bts-split" class="carousel slide" aria-roledescription="carousel" aria-label="Quillbase customer quotes">
            <div class="carousel-inner">
              <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
                <figure class="mb-0">
                  <blockquote class="bts-split-quote"><p>New hires used to ask the same twenty questions in Slack. Now they ask Quillbase and get the answer with a link to the source.</p></blockquote>
                  <figcaption class="d-flex align-items-center gap-3">
                    <svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#14532d"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">AV</text></svg>
                    <span><strong class="d-block">Amara Vance</strong><span class="bts-split-role">People Ops, Tidewell Health</span></span>
                  </figcaption>
                </figure>
              </div>
              <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
                <figure class="mb-0">
                  <blockquote class="bts-split-quote"><p>Support finds the right macro in seconds. First reply time went from four hours to under one.</p></blockquote>
                  <figcaption class="d-flex align-items-center gap-3">
                    <svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#1d4ed8"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">KB</text></svg>
                    <span><strong class="d-block">Kofi Boateng</strong><span class="bts-split-role">Support lead, Parcelly</span></span>
                  </figcaption>
                </figure>
              </div>
              <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
                <figure class="mb-0">
                  <blockquote class="bts-split-quote"><p>We imported eight years of wiki pages on a Friday. On Monday the sales team had stopped asking engineering for specs.</p></blockquote>
                  <figcaption class="d-flex align-items-center gap-3">
                    <svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#7c2d12"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">SL</text></svg>
                    <span><strong class="d-block">Sofia Lindgren</strong><span class="bts-split-role">VP Engineering, Orbitform</span></span>
                  </figcaption>
                </figure>
              </div>
            </div>
            <div class="carousel-indicators">
              <button type="button" data-bs-target="#bts-split" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Quote 1"></button>
              <button type="button" data-bs-target="#bts-split" data-bs-slide-to="1" aria-label="Quote 2"></button>
              <button type="button" data-bs-target="#bts-split" data-bs-slide-to="2" aria-label="Quote 3"></button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Several testimonials, cards and navigation

Layouts that show more than one voice: three cards per slide that regroup to one on phones, a scroll-snap row without the carousel plugin, video-style cards with a transcript modal and a row of customer thumbnails that drives the slider.

Three cards per slide, one on phones

  • matchMedia
  • h-100 cards
  • row g-3
  • createElement

Use the arrows. Resize the window across 768px to see the slides regroup.

The carousel has no items-per-view option, so the script regroups the same card elements into slides: three per carousel-item from 768px up, one per slide below. It moves the existing nodes with appendChild (no HTML strings) and rebuilds on the matchMedia change event, so phones keep every testimonial instead of hiding two of them. Cards use h-100 for equal heights inside a slide.

View code
<div class="bts-grp">
  <div class="d-flex align-items-center justify-content-between gap-3 mb-3">
    <h2 class="bts-grp-title">Learners on the Fernwood design courses</h2>
    <div class="d-flex align-items-center gap-2 flex-shrink-0">
      <span class="bts-grp-status" aria-hidden="true">1 / 2</span>
      <button type="button" class="btn bts-grp-nav" data-bs-target="#bts-grp" data-bs-slide="prev"><i class="bi bi-chevron-left" aria-hidden="true"></i><span class="visually-hidden">Previous testimonials</span></button>
      <button type="button" class="btn bts-grp-nav" data-bs-target="#bts-grp" data-bs-slide="next"><i class="bi bi-chevron-right" aria-hidden="true"></i><span class="visually-hidden">Next testimonials</span></button>
    </div>
  </div>
  <div id="bts-grp" class="carousel slide" aria-roledescription="carousel" aria-label="Learner testimonials">
    <div class="carousel-inner">
      <div class="carousel-item active">
        <div class="row g-3">
          <div class="col-12 col-md-4">
            <figure class="card h-100 bts-grp-card"><div class="card-body">
              <p class="bts-grp-stars mb-2"><span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9733;</span><span class="visually-hidden">Rated 5 out of 5</span></p>
              <blockquote class="mb-3"><p>The typography module fixed habits I had for ten years. My client decks look calmer and I present them with more confidence.</p></blockquote>
              <figcaption><strong>Leah Moss</strong><span>Typography for Interfaces</span></figcaption>
            </div></figure>
          </div>
          <div class="col-12 col-md-4">
            <figure class="card h-100 bts-grp-card"><div class="card-body">
              <p class="bts-grp-stars mb-2"><span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9733;</span><span class="visually-hidden">Rated 5 out of 5</span></p>
              <blockquote class="mb-3"><p>Real briefs, real feedback within two days. I moved from print to product design and landed a junior role in March.</p></blockquote>
              <figcaption><strong>Omar Haddad</strong><span>Product Design Bootcamp</span></figcaption>
            </div></figure>
          </div>
          <div class="col-12 col-md-4">
            <figure class="card h-100 bts-grp-card"><div class="card-body">
              <p class="bts-grp-stars mb-2"><span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9734;</span><span class="visually-hidden">Rated 4 out of 5</span></p>
              <blockquote class="mb-3"><p>Dense in places, but the grid exercises are gold. I rebuilt our design system spacing scale during week three.</p></blockquote>
              <figcaption><strong>Ines Duarte</strong><span>Layout and Grids</span></figcaption>
            </div></figure>
          </div>
        </div>
      </div>
      <div class="carousel-item">
        <div class="row g-3">
          <div class="col-12 col-md-4">
            <figure class="card h-100 bts-grp-card"><div class="card-body">
              <p class="bts-grp-stars mb-2"><span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9733;</span><span class="visually-hidden">Rated 5 out of 5</span></p>
              <blockquote class="mb-3"><p>The accessibility course paid off on day one. Our audit list went from forty issues to six in a single sprint.</p></blockquote>
              <figcaption><strong>Daniel Okoro</strong><span>Accessible UI Patterns</span></figcaption>
            </div></figure>
          </div>
          <div class="col-12 col-md-4">
            <figure class="card h-100 bts-grp-card"><div class="card-body">
              <p class="bts-grp-stars mb-2"><span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9733;</span><span class="visually-hidden">Rated 5 out of 5</span></p>
              <blockquote class="mb-3"><p>Short lessons I could finish on the train. The color module finally made contrast ratios click for me.</p></blockquote>
              <figcaption><strong>Mei Lin</strong><span>Color for Screens</span></figcaption>
            </div></figure>
          </div>
          <div class="col-12 col-md-4">
            <figure class="card h-100 bts-grp-card"><div class="card-body">
              <p class="bts-grp-stars mb-2"><span aria-hidden="true">&#9733;&#9733;&#9733;&#9733;&#9734;</span><span class="visually-hidden">Rated 4 out of 5</span></p>
              <blockquote class="mb-3"><p>Great mentors. I wanted more on motion, but the prototyping lessons covered what my team actually ships.</p></blockquote>
              <figcaption><strong>Kai Petersen</strong><span>Prototyping in the Browser</span></figcaption>
            </div></figure>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Scroll-snap row with previous and next buttons

  • scroll-snap-type
  • scrollBy()
  • list-unstyled
  • no carousel plugin

Use the arrow buttons, swipe, or focus the row and use the arrow keys.

Not every testimonial row needs the carousel plugin. This one is a list in a horizontally scrolling region with CSS scroll snapping: three cards fit from 768px up, one and a bit on phones so the next card peeks in. The buttons call scrollBy() by one card width, switch to an instant jump under reduced motion and disable themselves at either end. Swipe, trackpad and keyboard scrolling work natively.

View code
<div class="bts-snap" id="bts-snap">
  <div class="d-flex align-items-end justify-content-between gap-3 mb-3">
    <h2 class="bts-snap-title">Clients on working with Studio Ferro</h2>
    <div class="d-flex gap-2 flex-shrink-0">
      <button type="button" class="btn bts-snap-nav bts-snap-prev" aria-controls="bts-snap-track"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="visually-hidden">Scroll to previous quotes</span></button>
      <button type="button" class="btn bts-snap-nav bts-snap-next" aria-controls="bts-snap-track"><i class="bi bi-arrow-right" aria-hidden="true"></i><span class="visually-hidden">Scroll to next quotes</span></button>
    </div>
  </div>
  <div class="bts-snap-track" id="bts-snap-track" role="region" aria-label="Client quotes" tabindex="0">
    <ul class="list-unstyled bts-snap-list">
      <li class="bts-snap-card bts-snap-c1">
        <figure class="mb-0 h-100 d-flex flex-column">
          <blockquote class="flex-grow-1"><p>They redesigned our booking flow in six weeks and mobile conversions went up the month it launched.</p></blockquote>
          <figcaption><strong>Rosa Medina</strong><span>Founder, Lantern Yoga</span></figcaption>
        </figure>
      </li>
      <li class="bts-snap-card bts-snap-c2">
        <figure class="mb-0 h-100 d-flex flex-column">
          <blockquote class="flex-grow-1"><p>The only agency that asked to see our support tickets before designing anything. It showed in the result.</p></blockquote>
          <figcaption><strong>Arjun Mehta</strong><span>CTO, Dockyard Pay</span></figcaption>
        </figure>
      </li>
      <li class="bts-snap-card bts-snap-c3">
        <figure class="mb-0 h-100 d-flex flex-column">
          <blockquote class="flex-grow-1"><p>Our menu site finally loads on the bad signal inside the restaurant. Guests stopped asking for paper menus.</p></blockquote>
          <figcaption><strong>Bea Castell</strong><span>Owner, Osteria Due Ponti</span></figcaption>
        </figure>
      </li>
      <li class="bts-snap-card bts-snap-c4">
        <figure class="mb-0 h-100 d-flex flex-column">
          <blockquote class="flex-grow-1"><p>Clear weekly demos, no surprises on the invoice, and a handover doc our own developers actually read.</p></blockquote>
          <figcaption><strong>Henrik Dahl</strong><span>Product lead, Sundial Energy</span></figcaption>
        </figure>
      </li>
      <li class="bts-snap-card bts-snap-c5">
        <figure class="mb-0 h-100 d-flex flex-column">
          <blockquote class="flex-grow-1"><p>They pushed back on our idea for a homepage video, and they were right. The simpler page converts better.</p></blockquote>
          <figcaption><strong>Chloe Martin</strong><span>Marketing, Fable Bikes</span></figcaption>
        </figure>
      </li>
    </ul>
  </div>
</div>

Video testimonial slides with a transcript modal

  • ratio ratio-4x3
  • show.bs.modal relatedTarget
  • object-fit-cover
  • carousel-indicators

Press the play button on a poster to open the transcript.

Each slide pairs a poster still with a short pull quote. The whole poster is one button that opens a single shared modal, and the show.bs.modal event reads event.relatedTarget to fill in the name, still and transcript, which is the varying modal content pattern from the Bootstrap docs. In production you would put a video element with controls where the still sits; the transcript stays either way.

View code
<div class="bts-vid">
  <h2 class="bts-vid-title">Travelers on their Northbound trips</h2>
  <div id="bts-vid" class="carousel slide" aria-roledescription="carousel" aria-label="Video testimonials">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <div class="row g-3 g-md-4 align-items-center">
          <div class="col-md-6">
            <div class="ratio ratio-4x3 bts-vid-poster">
              <img src="mountain-hiker.webp" class="object-fit-cover" width="800" height="600" alt="Still from the video: a hiker with a large backpack on a rocky ridge above a hazy valley">
              <button type="button" class="bts-vid-play" data-bs-toggle="modal" data-bs-target="#bts-vid-modal" data-bts-name="Lena Varga, Alpine ridge week" data-bts-text="I booked the ridge week alone and was nervous about the pace. Our guide split the group on day two so nobody had to rush, and the hut on the third night had the best soup I have eaten. I am already looking at the autumn dates."><span class="bts-vid-circle"><i class="bi bi-play-fill" aria-hidden="true"></i></span><span class="bts-vid-dur">1:48</span><span class="visually-hidden">Open video testimonial from Lena Varga, 1 minute 48 seconds</span></button>
            </div>
          </div>
          <div class="col-md-6">
            <figure class="mb-0">
              <blockquote class="bts-vid-quote"><p>Our guide split the group on day two so nobody had to rush.</p></blockquote>
              <figcaption><strong>Lena Varga</strong><span>Alpine ridge week, July</span></figcaption>
            </figure>
          </div>
        </div>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <div class="row g-3 g-md-4 align-items-center">
          <div class="col-md-6">
            <div class="ratio ratio-4x3 bts-vid-poster">
              <img src="snow-camp.webp" class="object-fit-cover" width="800" height="600" alt="Still from the video: a yellow tent pitched in deep snow below jagged white peaks" loading="lazy">
              <button type="button" class="bts-vid-play" data-bs-toggle="modal" data-bs-target="#bts-vid-modal" data-bts-name="Theo Brandt, Winter camp course" data-bts-text="I had never slept in snow. The first evening was all practice: pitching the tent, building a wall against the wind, melting water. By night three I was the one showing the newcomers how to stake a tent in powder."><span class="bts-vid-circle"><i class="bi bi-play-fill" aria-hidden="true"></i></span><span class="bts-vid-dur">2:05</span><span class="visually-hidden">Open video testimonial from Theo Brandt, 2 minutes 5 seconds</span></button>
            </div>
          </div>
          <div class="col-md-6">
            <figure class="mb-0">
              <blockquote class="bts-vid-quote"><p>By night three I was showing the newcomers how to stake a tent in powder.</p></blockquote>
              <figcaption><strong>Theo Brandt</strong><span>Winter camp course, February</span></figcaption>
            </figure>
          </div>
        </div>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <div class="row g-3 g-md-4 align-items-center">
          <div class="col-md-6">
            <div class="ratio ratio-4x3 bts-vid-poster">
              <img src="canal-houses.webp" class="object-fit-cover" width="800" height="600" alt="Still from the video: small boats moored on a canal lined with colorful old houses" loading="lazy">
              <button type="button" class="bts-vid-play" data-bs-toggle="modal" data-bs-target="#bts-vid-modal" data-bts-name="Priya and Sam Nair, Canal towns by bike" data-bts-text="We wanted something slower after a busy year. Forty kilometers a day, luggage moved for us, and every town had a canal to eat beside. The route notes were so good we never once opened a map app."><span class="bts-vid-circle"><i class="bi bi-play-fill" aria-hidden="true"></i></span><span class="bts-vid-dur">1:32</span><span class="visually-hidden">Open video testimonial from Priya and Sam Nair, 1 minute 32 seconds</span></button>
            </div>
          </div>
          <div class="col-md-6">
            <figure class="mb-0">
              <blockquote class="bts-vid-quote"><p>The route notes were so good we never once opened a map app.</p></blockquote>
              <figcaption><strong>Priya and Sam Nair</strong><span>Canal towns by bike, May</span></figcaption>
            </figure>
          </div>
        </div>
      </div>
    </div>
    <div class="carousel-indicators">
      <button type="button" data-bs-target="#bts-vid" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Video 1"></button>
      <button type="button" data-bs-target="#bts-vid" data-bs-slide-to="1" aria-label="Video 2"></button>
      <button type="button" data-bs-target="#bts-vid" data-bs-slide-to="2" aria-label="Video 3"></button>
    </div>
  </div>
  <div class="modal fade" id="bts-vid-modal" tabindex="-1" aria-labelledby="bts-vid-modal-title" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered">
      <div class="modal-content">
        <div class="modal-header">
          <h2 class="modal-title fs-6" id="bts-vid-modal-title">Video testimonial</h2>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <img class="bts-vid-still d-block w-100 rounded-2 mb-3" src="mountain-hiker.webp" width="800" height="600" alt="">
          <h3 class="h6 mb-2">Transcript</h3>
          <p class="bts-vid-transcript mb-0"></p>
        </div>
      </div>
    </div>
  </div>
</div>

Customer thumbnails that drive the slider

  • data-bs-slide-to
  • slide.bs.carousel
  • aria-current
  • d-none d-sm-block

Any element with data-bs-target and data-bs-slide-to moves the carousel, even outside .carousel-indicators, but Bootstrap only syncs .active and aria-current on its own indicators. Here a small slide.bs.carousel listener moves both onto the matching thumbnail. On phones the thumbnails shrink to avatars and keep their accessible names through aria-label.

View code
<div class="bts-thumb">
  <div id="bts-thumb" class="carousel slide" aria-roledescription="carousel" aria-label="Strideline runner stories">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <figure class="mb-0">
          <blockquote class="bts-thumb-quote"><p>The plan moved my long run when I got sick instead of pretending the week never happened. I finished my first marathon in 3:41.</p></blockquote>
          <figcaption class="bts-thumb-cap">Nadia Petrova, first marathon</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <figure class="mb-0">
          <blockquote class="bts-thumb-quote"><p>I came back from a knee injury with the return-to-run program. Twelve weeks later I ran a 10K without a single twinge.</p></blockquote>
          <figcaption class="bts-thumb-cap">Marcus Bell, 10K comeback</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <figure class="mb-0">
          <blockquote class="bts-thumb-quote"><p>Easy days are actually easy now. My heart rate zones finally make sense, and I stopped racing every Tuesday run.</p></blockquote>
          <figcaption class="bts-thumb-cap">Yuki Tanaka, half marathon</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <figure class="mb-0">
          <blockquote class="bts-thumb-quote"><p>Couch to 5K at 52. The audio cues kept me honest, and my daughter and I now run the Saturday parkrun together.</p></blockquote>
          <figcaption class="bts-thumb-cap">Gwen Harper, first 5K</figcaption>
        </figure>
      </div>
    </div>
  </div>
  <div class="bts-thumb-list" role="group" aria-label="Choose a runner">
    <button type="button" class="bts-thumb-btn is-active" data-bs-target="#bts-thumb" data-bs-slide-to="0" aria-current="true" aria-label="Nadia Petrova"><svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#c2410c"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">NP</text></svg><span class="d-none d-sm-block text-start"><strong>Nadia Petrova</strong><span>Marathon, 3:41</span></span></button>
    <button type="button" class="bts-thumb-btn" data-bs-target="#bts-thumb" data-bs-slide-to="1" aria-label="Marcus Bell"><svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#1d4ed8"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">MB</text></svg><span class="d-none d-sm-block text-start"><strong>Marcus Bell</strong><span>10K, 48:10</span></span></button>
    <button type="button" class="bts-thumb-btn" data-bs-target="#bts-thumb" data-bs-slide-to="2" aria-label="Yuki Tanaka"><svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#15803d"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">YT</text></svg><span class="d-none d-sm-block text-start"><strong>Yuki Tanaka</strong><span>Half, 1:44</span></span></button>
    <button type="button" class="bts-thumb-btn" data-bs-target="#bts-thumb" data-bs-slide-to="3" aria-label="Gwen Harper"><svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#6d28d9"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">GH</text></svg><span class="d-none d-sm-block text-start"><strong>Gwen Harper</strong><span>5K, 31:02</span></span></button>
  </div>
</div>

Autoplay, vertical slides and a marquee

Moving testimonials done the accessible way: a visible pause button for the autoplaying slider, a vertical transition made with two transform overrides, and a CSS marquee that stops on hover, on focus and on request, and never moves under reduced motion.

Autoplay with a pause button and per-slide intervals

  • data-bs-interval
  • cycle() / pause()
  • aria-live
  • prefers-reduced-motion

Press the pause button, or tab into the slider to stop it.

An auto-advancing slider that meets WCAG 2.2.2: the pause button comes first in the tab order, and focus inside the slider stops rotation until the visitor presses play again. Each carousel-item sets its own data-bs-interval, longer for longer quotes. The instance is created with pause: false so the script owns hover and focus behavior, aria-live switches to polite while paused, and under reduced motion the slider starts paused.

View code
<div id="bts-auto" class="carousel slide bts-auto" aria-roledescription="carousel" aria-label="Member stories from Loftline Workspaces">
  <div class="bts-auto-bar">
    <button type="button" class="btn bts-auto-toggle"><i class="bi bi-pause-fill" aria-hidden="true"></i><span class="bts-auto-label">Pause quotes</span></button>
    <span class="bts-auto-state" aria-hidden="true">Playing</span>
  </div>
  <div class="carousel-inner" aria-live="off">
    <div class="carousel-item active" data-bs-interval="7000" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <figure class="bts-auto-fig">
        <blockquote class="bts-auto-quote"><p>Fast wifi, phone booths that are actually quiet, and coffee nobody has to apologize for.</p></blockquote>
        <figcaption><strong>Ade Okonkwo</strong><span>Freelance data analyst</span></figcaption>
      </figure>
    </div>
    <div class="carousel-item" data-bs-interval="10000" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <figure class="bts-auto-fig">
        <blockquote class="bts-auto-quote"><p>We grew from three desks to a private room for twelve without signing a new lease. The move took one afternoon.</p></blockquote>
        <figcaption><strong>Clara Jensen</strong><span>Co-founder, Pollen Analytics</span></figcaption>
      </figure>
    </div>
    <div class="carousel-item" data-bs-interval="8000" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <figure class="bts-auto-fig">
        <blockquote class="bts-auto-quote"><p>I came for a desk and stayed for the Friday lunches. Half my clients now are people I met in the kitchen.</p></blockquote>
        <figcaption><strong>Rafael Ortiz</strong><span>Brand designer</span></figcaption>
      </figure>
    </div>
  </div>
  <div class="bts-auto-foot">
    <button type="button" class="btn bts-auto-nav" data-bs-target="#bts-auto" data-bs-slide="prev"><i class="bi bi-chevron-left" aria-hidden="true"></i><span class="visually-hidden">Previous quote</span></button>
    <button type="button" class="btn bts-auto-nav" data-bs-target="#bts-auto" data-bs-slide="next"><i class="bi bi-chevron-right" aria-hidden="true"></i><span class="visually-hidden">Next quote</span></button>
  </div>
</div>

Vertical slide in a sidebar widget

  • carousel-item-next
  • translateY
  • fixed-height carousel-inner
  • data-bs-slide

Use the up and down buttons.

Bootstrap moves slides with translateX on the carousel-item-next, carousel-item-prev, carousel-item-start and carousel-item-end classes. Overriding those four selectors with translateY, scoped to this widget, turns the slide vertical while Bootstrap keeps its own transition and its reduced-motion rule. A vertical slide needs a fixed height on carousel-inner, so the quotes are kept short.

View code
<div class="bts-vert">
  <div class="d-flex align-items-center justify-content-between gap-2 mb-3">
    <h2 class="bts-vert-title">Readers of The Margin Note</h2>
    <div class="d-flex gap-1">
      <button type="button" class="btn bts-vert-nav" data-bs-target="#bts-vert" data-bs-slide="prev"><i class="bi bi-chevron-up" aria-hidden="true"></i><span class="visually-hidden">Previous reader quote</span></button>
      <button type="button" class="btn bts-vert-nav" data-bs-target="#bts-vert" data-bs-slide="next"><i class="bi bi-chevron-down" aria-hidden="true"></i><span class="visually-hidden">Next reader quote</span></button>
    </div>
  </div>
  <div id="bts-vert" class="carousel slide" aria-roledescription="carousel" aria-label="Reader quotes">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <figure class="mb-0">
          <blockquote class="bts-vert-quote"><p>The only newsletter I read the day it lands. Short, sharp and never a hot take for its own sake.</p></blockquote>
          <figcaption>Ama Mensah, reader since 2022</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <figure class="mb-0">
          <blockquote class="bts-vert-quote"><p>I forward one issue a month to my whole team. The pricing teardown series alone was worth it.</p></blockquote>
          <figcaption>Luis Ferreira, product manager</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <figure class="mb-0">
          <blockquote class="bts-vert-quote"><p>Finally someone who shows the spreadsheet behind the opinion. I trust the numbers here.</p></blockquote>
          <figcaption>Ruth Eriksen, analyst</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <figure class="mb-0">
          <blockquote class="bts-vert-quote"><p>Five minutes on Sunday morning and I know what actually happened in fintech that week.</p></blockquote>
          <figcaption>Dev Malhotra, founder</figcaption>
        </figure>
      </div>
    </div>
  </div>
  <a href="#" class="btn bts-vert-cta">Subscribe free</a>
</div>

Two-row testimonial marquee with a pause toggle

  • @keyframes
  • animation-play-state
  • aria-pressed
  • prefers-reduced-motion

Hover a row, or press Pause scrolling quotes.

A pure CSS marquee: each row holds the list twice (the copy is aria-hidden) and slides by -50% in a loop, the second row in reverse. The animation only exists inside prefers-reduced-motion: no-preference and stops on hover, on keyboard focus and with the toggle button, which uses aria-pressed and covers WCAG 2.2.2. With reduced motion the rows become plain horizontal scrollers and the toggle is hidden.

View code
<div class="bts-mq" id="bts-mq">
  <div class="container">
    <div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
      <h2 class="bts-mq-title">Support teams running on Brightdesk</h2>
      <button type="button" class="btn bts-mq-toggle" aria-pressed="false"><i class="bi bi-pause-fill" aria-hidden="true"></i><span>Pause scrolling quotes</span></button>
    </div>
  </div>
  <div class="bts-mq-row" role="region" aria-label="Customer quotes, first row" tabindex="0">
    <div class="bts-mq-track">
      <ul class="bts-mq-list">
        <li><figure><blockquote><p>Our backlog went from 900 tickets to 40 in a month.</p></blockquote><figcaption><strong>Iris Novak</strong><span>Lumen Rail</span></figcaption></figure></li>
        <li><figure><blockquote><p>Macros that agents actually like using. That never happened before.</p></blockquote><figcaption><strong>Ben Asante</strong><span>Copperleaf Bank</span></figcaption></figure></li>
        <li><figure><blockquote><p>Setup took one afternoon, including the email import.</p></blockquote><figcaption><strong>Sara Holm</strong><span>Nordic Bikes</span></figcaption></figure></li>
        <li><figure><blockquote><p>Customers rate us 4.8 now. It was 3.9 last spring.</p></blockquote><figcaption><strong>Luca Romano</strong><span>Pastaio Online</span></figcaption></figure></li>
      </ul>
      <ul class="bts-mq-list bts-mq-dup" aria-hidden="true">
        <li><figure><blockquote><p>Our backlog went from 900 tickets to 40 in a month.</p></blockquote><figcaption><strong>Iris Novak</strong><span>Lumen Rail</span></figcaption></figure></li>
        <li><figure><blockquote><p>Macros that agents actually like using. That never happened before.</p></blockquote><figcaption><strong>Ben Asante</strong><span>Copperleaf Bank</span></figcaption></figure></li>
        <li><figure><blockquote><p>Setup took one afternoon, including the email import.</p></blockquote><figcaption><strong>Sara Holm</strong><span>Nordic Bikes</span></figcaption></figure></li>
        <li><figure><blockquote><p>Customers rate us 4.8 now. It was 3.9 last spring.</p></blockquote><figcaption><strong>Luca Romano</strong><span>Pastaio Online</span></figcaption></figure></li>
      </ul>
    </div>
  </div>
  <div class="bts-mq-row bts-mq-rev" role="region" aria-label="Customer quotes, second row" tabindex="0">
    <div class="bts-mq-track">
      <ul class="bts-mq-list">
        <li><figure><blockquote><p>The shared inbox finally stopped double replies.</p></blockquote><figcaption><strong>Maya Cohen</strong><span>Harbor Dental</span></figcaption></figure></li>
        <li><figure><blockquote><p>Night shift handover is a note in the ticket, not a phone call.</p></blockquote><figcaption><strong>Tariq Aziz</strong><span>Skyline Hostels</span></figcaption></figure></li>
        <li><figure><blockquote><p>Reports my CFO reads without asking me what they mean.</p></blockquote><figcaption><strong>Emma Vogel</strong><span>Alpenkraft</span></figcaption></figure></li>
        <li><figure><blockquote><p>We answer in Spanish and French without hiring twice.</p></blockquote><figcaption><strong>Diego Paz</strong><span>Cerro Travel</span></figcaption></figure></li>
      </ul>
      <ul class="bts-mq-list bts-mq-dup" aria-hidden="true">
        <li><figure><blockquote><p>The shared inbox finally stopped double replies.</p></blockquote><figcaption><strong>Maya Cohen</strong><span>Harbor Dental</span></figcaption></figure></li>
        <li><figure><blockquote><p>Night shift handover is a note in the ticket, not a phone call.</p></blockquote><figcaption><strong>Tariq Aziz</strong><span>Skyline Hostels</span></figcaption></figure></li>
        <li><figure><blockquote><p>Reports my CFO reads without asking me what they mean.</p></blockquote><figcaption><strong>Emma Vogel</strong><span>Alpenkraft</span></figcaption></figure></li>
        <li><figure><blockquote><p>We answer in Spanish and French without hiring twice.</p></blockquote><figcaption><strong>Diego Paz</strong><span>Cerro Travel</span></figcaption></figure></li>
      </ul>
    </div>
  </div>
</div>

Dark mode, rating summaries and theming

Testimonial sliders that lean on the Bootstrap 5.3 color modes and CSS variables: a dark slider that flips with data-bs-theme, a review summary with progress bars, text-only company wordmarks as indicators and chat-bubble quotes themed through --bs-border-radius.

Dark mode slider with a color mode switch

  • data-bs-theme
  • bg-body-tertiary
  • var(--bs-emphasis-color)
  • form-switch

Use the arrows, or switch the card to light mode.

Built only from color mode aware tokens (bg-body-tertiary, text-body-secondary, border and --bs-emphasis-color), so the same markup works in light and dark. The switch flips data-bs-theme on the wrapper. Bootstrap 5.3 turns carousel controls and indicators black under a dark theme, which vanishes on a dark card, so the dots are recolored with --bs-emphasis-color and the arrows are themed buttons.

View code
<div class="bts-drk bg-body-tertiary border" data-bs-theme="dark">
  <div class="d-flex flex-wrap align-items-center justify-content-between gap-3 mb-4">
    <h2 class="bts-drk-title">Developers on Lintel code review</h2>
    <div class="form-check form-switch mb-0">
      <input class="form-check-input" type="checkbox" role="switch" id="bts-drk-mode" checked>
      <label class="form-check-label" for="bts-drk-mode">Dark mode</label>
    </div>
  </div>
  <div id="bts-drk" class="carousel slide" aria-roledescription="carousel" aria-label="Developer quotes">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <figure class="mb-0">
          <blockquote class="bts-drk-quote"><p>Lintel flags the <code>useEffect</code> without a cleanup before a human reviewer even opens the pull request. Reviews are about design again.</p></blockquote>
          <figcaption class="d-flex align-items-center gap-3">
            <svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#6d28d9"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">ZK</text></svg>
            <span><strong class="d-block">Zoe Kim</strong><span class="text-body-secondary small">Staff engineer, Ferrous Cloud</span></span>
          </figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <figure class="mb-0">
          <blockquote class="bts-drk-quote"><p>Median time to merge dropped from 31 hours to 9. The suggested fixes are small enough that people just accept them.</p></blockquote>
          <figcaption class="d-flex align-items-center gap-3">
            <svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#0f766e"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">AO</text></svg>
            <span><strong class="d-block">Abel Osei</strong><span class="text-body-secondary small">Engineering manager, Quayside</span></span>
          </figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <figure class="mb-0">
          <blockquote class="bts-drk-quote"><p>It runs on our own runners, so the code never leaves the building. That was the only way legal would say yes.</p></blockquote>
          <figcaption class="d-flex align-items-center gap-3">
            <svg viewBox="0 0 48 48" width="44" height="44" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#b45309"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">MS</text></svg>
            <span><strong class="d-block">Mira Soto</strong><span class="text-body-secondary small">Platform lead, Kestrel Health</span></span>
          </figcaption>
        </figure>
      </div>
    </div>
    <div class="d-flex align-items-center justify-content-between gap-3 mt-4">
      <div class="carousel-indicators">
        <button type="button" data-bs-target="#bts-drk" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Quote 1"></button>
        <button type="button" data-bs-target="#bts-drk" data-bs-slide-to="1" aria-label="Quote 2"></button>
        <button type="button" data-bs-target="#bts-drk" data-bs-slide-to="2" aria-label="Quote 3"></button>
      </div>
      <div class="d-flex gap-2">
        <button type="button" class="btn bts-drk-nav" data-bs-target="#bts-drk" data-bs-slide="prev"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="visually-hidden">Previous quote</span></button>
        <button type="button" class="btn bts-drk-nav" data-bs-target="#bts-drk" data-bs-slide="next"><i class="bi bi-arrow-right" aria-hidden="true"></i><span class="visually-hidden">Next quote</span></button>
      </div>
    </div>
  </div>
</div>

Rating summary with bars and a review slider

  • .progress
  • --bs-progress-bar-bg
  • role="progressbar"
  • col-md-5

Use the arrows or the indicators.

The left column is a review summary: the average score, stars with a visually-hidden equivalent and a breakdown built from Bootstrap progress bars, each with role, aria-valuenow and an aria-label, recolored through --bs-progress-bar-bg. The right column is a plain carousel of recent reviews with its own buttons, so the summary stays visible while the reviews change.

View code
<div class="bts-sum">
  <div class="row g-4 align-items-start">
    <div class="col-md-5">
      <h2 class="bts-sum-title">Lumo Arc desk lamp</h2>
      <p class="bts-sum-score mb-1"><span class="bts-sum-big">4.5</span><span class="visually-hidden"> out of 5 stars</span><span class="bts-sum-stars" aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-half"></i></span></p>
      <p class="bts-sum-count">Based on 312 reviews</p>
      <ul class="list-unstyled bts-sum-bars mb-0">
        <li><span class="bts-sum-lbl">5 stars</span><div class="progress" role="progressbar" aria-label="5 stars" aria-valuenow="71" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 71%"></div></div><span class="bts-sum-pct">71%</span></li>
        <li><span class="bts-sum-lbl">4 stars</span><div class="progress" role="progressbar" aria-label="4 stars" aria-valuenow="18" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 18%"></div></div><span class="bts-sum-pct">18%</span></li>
        <li><span class="bts-sum-lbl">3 stars</span><div class="progress" role="progressbar" aria-label="3 stars" aria-valuenow="6" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 6%"></div></div><span class="bts-sum-pct">6%</span></li>
        <li><span class="bts-sum-lbl">2 stars</span><div class="progress" role="progressbar" aria-label="2 stars" aria-valuenow="3" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 3%"></div></div><span class="bts-sum-pct">3%</span></li>
        <li><span class="bts-sum-lbl">1 star</span><div class="progress" role="progressbar" aria-label="1 star" aria-valuenow="2" aria-valuemin="0" aria-valuemax="100"><div class="progress-bar" style="width: 2%"></div></div><span class="bts-sum-pct">2%</span></li>
      </ul>
    </div>
    <div class="col-md-7">
      <div class="bts-sum-panel">
        <div class="d-flex align-items-center justify-content-between gap-2 mb-3">
          <h3 class="bts-sum-sub">Most helpful reviews</h3>
          <div class="d-flex gap-2">
            <button type="button" class="btn bts-sum-nav" data-bs-target="#bts-sum" data-bs-slide="prev"><i class="bi bi-chevron-left" aria-hidden="true"></i><span class="visually-hidden">Previous review</span></button>
            <button type="button" class="btn bts-sum-nav" data-bs-target="#bts-sum" data-bs-slide="next"><i class="bi bi-chevron-right" aria-hidden="true"></i><span class="visually-hidden">Next review</span></button>
          </div>
        </div>
        <div id="bts-sum" class="carousel slide" aria-roledescription="carousel" aria-label="Most helpful reviews">
          <div class="carousel-inner">
            <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
              <figure class="mb-0">
                <p class="bts-sum-rev-stars mb-2"><span aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i></span><span class="visually-hidden">Rated 5 out of 5</span></p>
                <blockquote class="bts-sum-quote"><p>The warm setting is perfect for late work without wrecking my sleep. The arm stays exactly where I leave it.</p></blockquote>
                <figcaption><strong>Hugo Laurent</strong><span>Bought in black, 3 weeks ago</span></figcaption>
              </figure>
            </div>
            <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
              <figure class="mb-0">
                <p class="bts-sum-rev-stars mb-2"><span aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star"></i></span><span class="visually-hidden">Rated 4 out of 5</span></p>
                <blockquote class="bts-sum-quote"><p>Even light across my whole drawing board. I would like a longer cable, but the clamp is rock solid.</p></blockquote>
                <figcaption><strong>Aiko Mori</strong><span>Bought in white, 2 months ago</span></figcaption>
              </figure>
            </div>
            <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
              <figure class="mb-0">
                <p class="bts-sum-rev-stars mb-2"><span aria-hidden="true"><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i><i class="bi bi-star-fill"></i></span><span class="visually-hidden">Rated 5 out of 5</span></p>
                <blockquote class="bts-sum-quote"><p>No flicker on camera, so it doubles as my video call light. The touch dimmer remembers the last level.</p></blockquote>
                <figcaption><strong>Samuel Okoye</strong><span>Bought in sage, 1 month ago</span></figcaption>
              </figure>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Text-only company wordmarks as indicators

  • carousel-indicators
  • text-indent reset
  • aria-current
  • system font stacks

Use the arrows or the indicators.

A trusted-by strip without anyone else's logo: the customer names are plain text wordmarks set in different system font stacks, and they are the carousel indicators. Keeping them inside .carousel-indicators means Bootstrap moves .active and aria-current itself; the CSS only resets the default 30px bar (width, height, text-indent, background) and adds an underline for the active company.

View code
<div class="bts-co">
  <div id="bts-co" class="carousel slide" aria-roledescription="carousel" aria-label="What customers say about Ordo invoicing">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 4">
        <figure class="mb-0">
          <blockquote class="bts-co-quote"><p>We send 2,000 invoices on the first of the month and Ordo chases the late ones for us. Days sales outstanding fell from 48 to 29.</p></blockquote>
          <figcaption><strong>Ingrid Halvorsen</strong>, Finance director at Halvorsen Marine</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 4">
        <figure class="mb-0">
          <blockquote class="bts-co-quote"><p>Wholesale orders come in through the shop, and the invoice is ready before the kiln has cooled.</p></blockquote>
          <figcaption><strong>Pia Brenner</strong>, Owner of kiln and co.</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 4">
        <figure class="mb-0">
          <blockquote class="bts-co-quote"><p>The API is boring in the best way. We bill per seat from our own app and never touch a PDF.</p></blockquote>
          <figcaption><strong>Theo Marrow</strong>, CTO at Marrow Studio</figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="4 of 4">
        <figure class="mb-0">
          <blockquote class="bts-co-quote"><p>Multi-currency invoices for eleven ports, one ledger. Our accountant sent flowers.</p></blockquote>
          <figcaption><strong>Kwame Asare</strong>, Operations at Bluefin Freight</figcaption>
        </figure>
      </div>
    </div>
    <div class="carousel-indicators bts-co-marks">
      <button type="button" data-bs-target="#bts-co" data-bs-slide-to="0" class="active bts-co-m1" aria-current="true">HALVORSEN</button>
      <button type="button" data-bs-target="#bts-co" data-bs-slide-to="1" class="bts-co-m2">kiln and co.</button>
      <button type="button" data-bs-target="#bts-co" data-bs-slide-to="2" class="bts-co-m3">marrow/studio</button>
      <button type="button" data-bs-target="#bts-co" data-bs-slide-to="3" class="bts-co-m4">Bluefin Freight</button>
    </div>
  </div>
</div>

Chat bubble quotes themed with CSS variables

  • --bs-border-radius
  • --bs-btn-*
  • carousel-fade
  • ::after tail

Use the arrows or the indicators.

Setting --bs-border-radius and --bs-border-radius-xl on the wrapper reshapes every Bootstrap piece inside it, because .btn and the utilities read those variables where they are used. The quotes are speech bubbles with a CSS triangle tail pointing at the initials avatar, and the coral buttons are themed only with --bs-btn-* variables. A crossfade suits bubbles better than a sideways slide.

View code
<div class="bts-chat">
  <div class="d-flex align-items-center justify-content-between gap-3 mb-3">
    <h2 class="bts-chat-title">Pet owners on their Pawpost sitters</h2>
    <span class="badge rounded-pill bts-chat-badge">2,400+ stays</span>
  </div>
  <div id="bts-chat" class="carousel slide carousel-fade" aria-roledescription="carousel" aria-label="Pet owner reviews">
    <div class="carousel-inner">
      <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <figure class="mb-0">
          <blockquote class="bts-chat-bubble rounded-4"><p>Our sitter sent a photo every evening, and Biscuit actually sulked when we came home. Highest compliment possible.</p></blockquote>
          <figcaption class="d-flex align-items-center gap-2">
            <svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#c2410c"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">JW</text></svg>
            <span><strong>Jess Ward</strong> <span class="bts-chat-meta">and Biscuit, beagle</span></span>
          </figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <figure class="mb-0">
          <blockquote class="bts-chat-bubble rounded-4"><p>Two cats, one on daily medication. Our sitter logged every dose in the app and the vet notes were spotless.</p></blockquote>
          <figcaption class="d-flex align-items-center gap-2">
            <svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#0f766e"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">RN</text></svg>
            <span><strong>Ravi Nair</strong> <span class="bts-chat-meta">and Miso and Udon, cats</span></span>
          </figcaption>
        </figure>
      </div>
      <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <figure class="mb-0">
          <blockquote class="bts-chat-bubble rounded-4"><p>Booked at 10 pm for a 7 am flight. Someone experienced with big dogs said yes within the hour.</p></blockquote>
          <figcaption class="d-flex align-items-center gap-2">
            <svg viewBox="0 0 48 48" width="40" height="40" aria-hidden="true" focusable="false"><circle cx="24" cy="24" r="24" fill="#1d4ed8"/><text x="24" y="30" text-anchor="middle" fill="#fff" font-size="17" font-weight="700">EM</text></svg>
            <span><strong>Elena Marsh</strong> <span class="bts-chat-meta">and Atlas, Newfoundland</span></span>
          </figcaption>
        </figure>
      </div>
    </div>
  </div>
  <div class="d-flex gap-2 mt-4">
    <button type="button" class="btn bts-chat-btn" data-bs-target="#bts-chat" data-bs-slide="prev"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="visually-hidden">Previous review</span></button>
    <button type="button" class="btn bts-chat-btn" data-bs-target="#bts-chat" data-bs-slide="next"><span>Next review</span><i class="bi bi-arrow-right ms-2" aria-hidden="true"></i></button>
  </div>
</div>

How to build a Bootstrap testimonial slider

It rests on the carousel component built into Bootstrap, plus the framework’s grid, card and utility classes for layout. Everything here targets Bootstrap 5.3.8, the current release in the official docs as of October 2026.

  1. Add the viewport meta tag, the CSS link in the head and the JavaScript before the closing body tag. The jsDelivr links in the Bootstrap docs include integrity hashes, so copy them along with the version number.
  2. Create the wrapper: one div with the classes carousel and slide, and a unique id.
  3. Put a carousel-inner div inside it, then one carousel-item per testimonial. The first item needs the active class, or the slider stays invisible (Bootstrap docs).
  4. Fill each slide with a figure, a blockquote and a figcaption, plus the avatar and rating.
  5. Wire up the controls and indicators. Every data-bs-target must match the wrapper id.
  6. Turn on autoplay last, once the manual version works.

Either the bundle or the standalone JavaScript file will do. Popper is only needed for dropdowns, popovers and tooltips, so the carousel doesn’t care which one you load.

Slide markup for one testimonial

Attribution goes outside the blockquote. Bootstrap’s typography docs say the HTML spec requires it, so the quote sits in a figure with a figcaption and the source name wrapped in cite.

<div id="testimonialSlider" class="carousel slide">
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#testimonialSlider" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Testimonial 1"></button>
    <button type="button" data-bs-target="#testimonialSlider" data-bs-slide-to="1" aria-label="Testimonial 2"></button>
  </div>
  <div class="carousel-inner">
    <div class="carousel-item active">
      <figure class="text-center px-5 pb-5 mb-0">
        <img src="avatar-1.jpg" class="rounded-circle mb-3" width="64" height="64" alt="Portrait of Customer Name">
        <blockquote class="blockquote">
          <p>Short, specific quote from the customer.</p>
        </blockquote>
        <figcaption class="blockquote-footer">
          Customer Name, <cite title="Company">Job title at Company</cite>
        </figcaption>
      </figure>
    </div>
    <div class="carousel-item">
      <!-- second testimonial, same structure, no active class -->
    </div>
  </div>
  <button class="carousel-control-prev" type="button" data-bs-target="#testimonialSlider" data-bs-slide="prev">
    <span class="carousel-control-prev-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Previous</span>
  </button>
  <button class="carousel-control-next" type="button" data-bs-target="#testimonialSlider" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Next</span>
  </button>
</div>

The px-5 and pb-5 padding keeps the quote clear of the arrows and the indicator dots.

Controls and indicators

Controls are button elements with data-bs-slide set to prev or next. The docs recommend buttons, though a link with role=”button” also works.

Indicators use data-bs-slide-to with a zero-based index, and the starting dot carries active and aria-current=”true”. Every data-bs-target has to match the wrapper id. That only starts to matter once a second slider lands on the same page, but it’s easier to get right from the beginning. The visually-hidden text names each arrow for screen readers, and the icon spans stay aria-hidden.

The default arrows are white, so they disappear on a light background. Set data-bs-theme=”dark” on the wrapper, because .carousel-dark has been deprecated since Bootstrap 5.3.0.

Each arrow zone takes 15% of the slider width by default ($carousel-control-width in the Sass variables). On a narrow slider that puts the arrows over the text unless the slide has side padding.

Star ratings and avatars

Stars are only decoration, so draw them with Bootstrap Icons, the Bootstrap team’s separate icon set, and mark each icon aria-hidden. Add a visually-hidden line such as “Rated 5 out of 5” next to them so the rating survives without the visuals.

Avatars should use rounded-circle with explicit width and height, so slides don’t shift when the image loads. The alt text is the customer’s name, not “avatar”.

Autoplay, timing and fade settings

Autoplay starts with data-bs-ride=”carousel”, cycles every 5000 ms by default and pauses while the pointer sits over the slider (Bootstrap docs, v5.3.8).

The default pause is “hover”, which stops on mouseenter and resumes on mouseleave. Ride defaults to false, so a slider without data-bs-ride never autoplays and only changes when the visitor acts. The transition itself takes 0.6 s, set by $carousel-transition-duration.

Autoplay and interval

The ride option decides who starts the motion. With “carousel” the slider autoplays on page load, while true makes it wait until the visitor changes a slide once.

A slider with data-bs-ride=”carousel” initializes itself, and creating it again with new bootstrap.Carousel() is a mistake the docs warn against. Without that attribute you get the opposite situation. The docs say carousels must be initialized manually, and the swipe listeners are not registered until the visitor first uses a control or indicator.

Quote length varies, so one timer rarely fits every slide. Put data-bs-interval on a carousel-item to give a long quote more time, for example 9000 for a 60-word testimonial.

Read the quote aloud once and add a few seconds. That’s an editor’s rule of thumb, not a documented value.

Pause, touch and wrap

With pause set to hover, a touch device pauses on touchend and waits two intervals before resuming. Swipe is on by default once the carousel is initialized (autoplaying carousels initialize themselves; a manual one needs new bootstrap.Carousel(‘#testimonialSlider’)), and data-bs-touch=”false” turns it off.

Wrap set to true loops forever, and false stops at the last slide. The carousel reacts to keyboard events unless keyboard is false, and cycling stops whenever the page is not visible, through the Page Visibility API.

Apart from the hidden-page stop, hover and the touchend pause on touch screens are the only automatic pause triggers Bootstrap documents. Keyboard and screen reader users get nothing from it, so the accessibility settings below matter.

Slide versus fade

Slide is the default. Adding carousel-fade swaps it for a crossfade.

Slide gives a direction cue that matches the previous and next arrows, and its speed comes from $carousel-transition-duration. Fade is calmer for text-only quotes and reads as less abrupt when the quotes differ in length. For plain text I’d pick fade, though text-only items may need .bg-body or a custom background to crossfade properly (Bootstrap docs).

Change the speed in Sass before compiling, or override the CSS if you use the compiled file. When a rule combines several transitions, define transform first.

How do you show several testimonials per slide?

Nest a row of cards inside each carousel-item. The carousel options (interval, keyboard, pause, ride, touch, wrap) include no items-per-view setting, so the layout comes from the grid and not from the plugin.

A row with row-cols-1 and row-cols-md-3 gives one card per row on phones and three across from the md breakpoint up. That split comes from the grid system underneath Bootstrap, and each slide holds one such row.

Keeping card heights equal

Carousels do not normalize slide dimensions (Bootstrap docs). Testimonials of different lengths make the slider jump in height on every change.

Build each quote as one of the cards built with Bootstrap and add h-100. Setting $card-height to 100% in Sass makes equal height the default for every card.

What breaks on small screens

Below md, row-cols-1 stacks the three cards of one slide on top of each other. The slide grows to three times its height.

Hiding cards two and three with d-none d-md-block fixes the height. It also removes those testimonials from phones entirely, because they only exist inside that slide.

The clean fix is one carousel-item per testimonial on small screens. That means a second markup set shown below md with display utilities, or a script that steps single cards.

Grid, extension or separate library

ApproachExtra codeMovement per clickMain limit
Grid inside each carousel-itemNoneOne full rowPhones stack or hide cards
Bootstrap 5 multi-item extensionJS and CSS add-onVaries by pluginDepends on a third-party plugin
Swiper or SplideSeparate libraryConfigurableA second slider API next to Bootstrap’s

Swiper’s API documentation lists slidesPerView and slidesPerGroup, along with a breakpoints parameter. Splide’s options guide covers perPage and perMove plus breakpoints, and its GitHub README says Splide is written in TypeScript with no dependencies.

A handful of short quotes fits the grid-in-slide approach. If you need looping or dragging, or want one card to step at a time, move to Swiper or Splide instead of patching the carousel.

Check what a multi-item snippet depends on before copying it. MDB’s support forum describes the multi-item carousel in its own kit as a premium feature, so a snippet labeled Bootstrap may not run on plain Bootstrap.

What accessibility work does a Bootstrap testimonial slider need?

Bootstrap covers hover pause, button controls with hidden labels, and reduced-motion handling. You still have to add a pause button, a stop on focus, live-region handling and the carousel roles.

BehaviorBootstrap 5.3.8W3C carousel patternWhat you add
Pause on hoverBuilt inRequiredNothing
Stop on keyboard focusNot described in the docsRequired, no automatic restartA focusin listener that pauses
Pause and start buttonNot built in, recommended by the docsRequired, first in tab orderA button that toggles pause and cycle
Live region on the slide wrapperNot describedOptional: off while rotating, polite when notToggle the attribute with the button
Container and slide rolesNot describedRole group or region, plus roledescriptionaria-roledescription and labels

Pause control and focus

A moving testimonial slider is subject to the usual web accessibility rules for moving content. WCAG 2.2 success criterion 2.2.2 (Level A) requires a pause, stop or hide mechanism for content that starts automatically, lasts more than five seconds and sits beside other content.

The W3C’s Understanding document (updated August 2026) says that stopping only while focus sits on the element does not count as a mechanism. Hover pause has the same shape, because it releases the moment the pointer leaves.

The ARIA Authoring Practices pattern differs in one detail. After focus enters the carousel, rotation stays stopped until the user restarts it. It also puts the rotation button first in the tab sequence and changes its label, in the APG example, between “Stop automatic slide show” and “Start automatic slide show”.

Bootstrap’s pause and cycle methods cover the button. Test a manual pause with the pointer moving out of the slider, since the documented hover behavior resumes cycling on mouseleave.

Labels, roles and live regions

Bootstrap’s indicator buttons already carry aria-label and aria-current. The rest of the pattern relies on ARIA roles that you write yourself.

The container gets role region or group, aria-roledescription=”carousel” and a label. Each slide gets role group and aria-roledescription=”slide”, labeled “3 of 10” when no unique name exists. The slide wrapper takes aria-live off while rotating and polite when it is not.

Reduced motion and contrast

Bootstrap disables the sliding animation in carousels when the visitor sets prefers-reduced-motion (Bootstrap accessibility docs). Those visitors get an instant swap, but the docs do not say the autoplay timer stops, so the pause button still matters.

The same docs warn that some default palette colors fall below the WCAG 2.2 text contrast minimum on light backgrounds. Test the quote text color against the slide background before shipping.

Bootstrap 4 vs Bootstrap 5 testimonial sliders

Build on Bootstrap 5. Bootstrap 4 reached end of life on January 1, 2023, according to the release schedule in the project’s twbs/release repository, and the v4.6 docs now open with an end-of-life banner.

The two versions also differ in markup, which is where copied testimonial snippets fail.

Part of the sliderBootstrap 4.6Bootstrap 5.3
JavaScriptjQuery plugin, $('.carousel').carousel()Vanilla JavaScript, jQuery optional
Autoplay and optionsdata-ride, data-intervaldata-bs-ride, data-bs-interval
Controls and indicatorsdata-target, data-slide, data-slide-todata-bs-target, data-bs-slide, data-bs-slide-to
Indicator markupol with li elementsdiv with button elements
Hidden label classsr-onlyvisually-hidden

Bootstrap 5 does not need jQuery, though it still works with it. If jQuery sits on the page, Bootstrap registers its plugins there automatically (Bootstrap 5.3 docs).

What breaks when you paste a Bootstrap 4 snippet

Many testimonial templates still target Bootstrap 4, and the failures are quiet. The slider renders, then misbehaves.

  • The v4 data attributes do nothing in v5, which reads only the data-bs- names, so controls and autoplay both die.
  • The sr-only class was renamed, so “Previous” and “Next” appear as visible text.
  • The .media component was dropped, which means avatar-plus-text layouts need flex utilities now.
  • .card-deck is gone in favor of the grid.
  • ml-* and mr-* became ms-* and me-*, and text-left became text-start.

A quick version check: if data-ride or data-target shows up anywhere in the markup, the snippet was written for Bootstrap 4.

When Bootstrap 4 is still the constraint

Bootstrap 5.0.0 dropped Internet Explorer 10 and 11 (Bootstrap migration docs). A site that still has to serve those browsers is stuck on Bootstrap 4, and that is a question of cross-browser compatibility, not of carousel features.

Teams that cannot migrate are pointed to HeroDevs’ Never-Ending Support program by that end-of-life banner in the Bootstrap 4.6 docs.

Why is the Bootstrap testimonial slider not working?

A slider that looks right and does nothing usually has one of a few easy-to-miss causes: Bootstrap 4 attributes on a Bootstrap 5 page, event listeners that seem silent when jQuery is on the page, calls fired mid-transition, or a slider sitting inside a hidden container.

Version mismatch and missing JavaScript

A Bootstrap 4 snippet on a Bootstrap 5 page shows the first testimonial and then sits still. The arrows do nothing and autoplay never starts. Previous and Next show up as plain text.

A missing JavaScript file produces the same freeze. The carousel is on Bootstrap’s list of components that require JavaScript, and the plugins have no special fallback when it is off (Bootstrap 5.3 docs).

Events that seem not to fire

The carousel docs tell you to listen for slid.bs.carousel with addEventListener. The JavaScript docs say that if jQuery is on the window and the body has no data-bs-no-jquery attribute, Bootstrap emits its events through jQuery’s event system, so listeners use .on().

Both pages describe the same plugin, and they disagree on the listener. In the 5.3.8 source, Bootstrap fires the jQuery event first and then still dispatches the native event, unless a jQuery handler calls stopImmediatePropagation(), so a plain addEventListener on the carousel element normally works. If a handler stays silent, look for a jQuery handler that stops the event, test the handler on your own page, and add data-bs-no-jquery to the body if you want the native version to win.

Calls ignored mid-transition

Programmatic calls are asynchronous, and a call on a component that is still transitioning is ignored (Bootstrap docs). In the 5.3.8 source that holds for next() and prev(), so custom buttons that fire them twice in a row lose the second step. to() is the exception: it waits for the running slide to finish, and the example below chains the jumps through slid.bs.carousel the way the docs do.

const el = document.querySelector('#testimonialSlider')
const carousel = bootstrap.Carousel.getOrCreateInstance(el)

el.addEventListener('slid.bs.carousel', () => carousel.to(2), { once: true })
carousel.to(1) // the jump to 2 waits for this transition to finish

Hidden containers, nesting and frameworks

A slider inside a collapsed accordion, tab or modal should advance with nextWhenVisible, which skips the move when the page, the carousel or its parent is not visible.

Nested carousels are not supported, so a testimonial slider inside another slide breaks.

Bootstrap’s JavaScript is not fully compatible with React, Vue and Angular, because both Bootstrap and the framework may mutate the same DOM element. Use the framework-specific package, such as React Bootstrap.

When is a testimonial slider the wrong choice?

A slider is the wrong choice when the quotes matter more than the space they save. Bootstrap’s own docs recommend avoiding autoplay and warn that carousels in general can cause usability and accessibility challenges.

The Bootstrap 4.6 docs went further and called carousels generally not compliant with accessibility standards. The 5.3 docs use softer wording.

What the usability research found

The usability findings on carousels have stayed consistent for years, and Nielsen Norman Group (NN/g) reviewed its carousel guidance again in August 2026.

Users ignore animated items that look like ads, and NN/g’s eyetracking found animated ads get looked at 27% of the time. Its advice is five or fewer frames, because people struggle to recognize what they have already seen beyond that. For reading time the guideline is 3 words per second.

That guideline puts a 60-word quote at 20 seconds. It is more than double the 9000 ms example used in the autoplay settings above, so use the longer figure.

In a usability study that NN/g wrote up in 2013, a participant could not find a prominent £100-off appliance offer on the Siemens UK homepage because the panel auto-rotated every five seconds. She gave up and assumed Siemens had no special deals.

A static grid as the alternative

Every quote visible at once beats a rotation nobody waits for. The cards from the multi-testimonial layout work without the carousel wrapper, with no controls and no timer.

On phones, NN/g advises against auto-forwarding. Pages are short, readers scroll fast, and by the time a slide changes they have often moved past the above-the-fold area.

NN/g also recommends repeating anything important from a carousel elsewhere in the page. For testimonials, that means the strongest quote as static text.

When a slider still earns its place

It earns a spot in a fixed-height band or sidebar where a grid will not fit, with five short quotes or fewer, manual controls only, and the best quote also shown as plain text.

Bootstrap testimonial slider FAQ

How do you swap the default arrows for custom icons?

Edit the icon, not the button. Keep the button element, its data-bs-slide attribute and the visually-hidden label, and change only the span inside it.

To restyle every slider at once, override $carousel-control-prev-icon-bg and $carousel-control-next-icon-bg in Sass.

Can the slider be controlled with JavaScript?

Yes. Fetch the instance with bootstrap.Carousel.getOrCreateInstance, then call cycle, pause, prev, next or to(index), where the index starts at 0.

The slide.bs.carousel and slid.bs.carousel events fire on the carousel element and report direction, relatedTarget, from and to (Bootstrap 5.3 docs).

Is there a ready-made package for a Bootstrap testimonial slider?

Yes. cdnjs lists a bootstrap-testimonial-slider library at version 1.0.0, described as a combination of the Bootstrap carousel and testimonials, with light, dark and right-to-left stylesheets.

The listing shows only CSS files, so the slide behavior still comes from Bootstrap’s own JavaScript.

How many testimonials should one slider hold, and how long can each quote be?

Five or fewer. Nielsen Norman Group’s carousel guidance (reviewed August 2026) sets that ceiling because people struggle to keep track of more frames.

Keep each quote short enough to read at about 3 words per second within its interval.

Which setup to ship first

Ship the Bootstrap testimonial slider with manual controls and no autoplay, then add motion only after testing it. That order matters because each step removes a failure the next one would hide.

Start with Bootstrap 5.3 markup and manual controls, initialized with new bootstrap.Carousel() so swipe works from the first visit. Then check keyboard, touch and reduced-motion behavior. Autoplay comes last, and only with a pause button.

Timing explains why autoplay goes last. Five 60-word quotes at 3 words per second take 100 seconds for one full rotation, so a visitor who leaves sooner never sees the whole set.

The downside is exposure. Quotes after the first get fewer views, because visitors must click to reach them.

As of October 29, 2026, Bootstrap 6.0.0-alpha.1 (released October 8, 2026) is the only v6 release, and 5.3 remains the current stable line. A stable v6 would mean rechecking the markup. The alpha makes the JavaScript ESM-only, so script tags need type=”module”, and it moves responsive class names from an infix to a prefix, so d-md-none becomes md:d-none.