Bootstrap Carousel: 16 Examples With Captions and Controls

Most Bootstrap carousel snippets online are written for the wrong version. Paste one into a Bootstrap 5 project and the slides just sit there, because Bootstrap 5.0.0 dropped jQuery and put a data-bs- prefix on every plugin data attribute.

Below are 16 Bootstrap carousel examples you can copy, built on 5.3.8, followed by a guide to the markup, autoplay, the JavaScript API 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>

Hero and photo sliders

Image carousels for the top of a page, a rental listing and a product page. Each one sizes its photos with object-fit-cover and an aspect ratio, because Bootstrap does not normalize slide heights for you.

Hero slider with captions and indicators

  • carousel-indicators
  • carousel-caption
  • object-fit-cover
  • getOrCreateInstance

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

The standard carousel markup with indicators, captions and the default controls. The photos get object-fit-cover and a CSS aspect ratio (4:3 on phones, 21:9 from md up), the first one loads with fetchpriority="high" and the rest lazily. The script creates the instance on load so swiping works before anyone touches a control, and the caption sits on a dark panel so the white text keeps its contrast on bright photos.

View code
<div id="bcr-hero" class="carousel slide bcr-hero">
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#bcr-hero" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
    <button type="button" data-bs-target="#bcr-hero" data-bs-slide-to="1" aria-label="Slide 2"></button>
    <button type="button" data-bs-target="#bcr-hero" data-bs-slide-to="2" aria-label="Slide 3"></button>
  </div>
  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="fjord.webp" class="d-block w-100 object-fit-cover bcr-hero-img" width="800" height="600" alt="A deep blue fjord between steep granite cliffs, seen from a rocky ledge" fetchpriority="high">
      <div class="carousel-caption bcr-hero-cap">
        <h2 class="h4 mb-1">Fjord ferries and cliff walks</h2>
        <p class="d-none d-md-block mb-3">Eight days on the west coast in a group of twelve, from 1,890 EUR.</p>
        <a href="#" class="btn btn-light d-none d-md-inline-block">See the route</a>
      </div>
    </div>
    <div class="carousel-item">
      <img src="red-rock-canyon.webp" class="d-block w-100 object-fit-cover bcr-hero-img" width="800" height="600" alt="Red sandstone cliffs glowing orange at sunset above a rocky plateau" loading="lazy">
      <div class="carousel-caption bcr-hero-cap">
        <h2 class="h4 mb-1">Canyon country by camper</h2>
        <p class="d-none d-md-block mb-3">Ten days, two national parks, campervan and permits included.</p>
        <a href="#" class="btn btn-light d-none d-md-inline-block">See the route</a>
      </div>
    </div>
    <div class="carousel-item">
      <img src="city-skyline.webp" class="d-block w-100 object-fit-cover bcr-hero-img" width="800" height="600" alt="A city skyline at sunrise beside a wide lake" loading="lazy">
      <div class="carousel-caption bcr-hero-cap">
        <h2 class="h4 mb-1">A weekend above the lake</h2>
        <p class="d-none d-md-block mb-3">Three nights in a rooftop hotel with an architecture boat tour.</p>
        <a href="#" class="btn btn-light d-none d-md-inline-block">See the route</a>
      </div>
    </div>
  </div>
  <button class="carousel-control-prev" type="button" data-bs-target="#bcr-hero" 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="#bcr-hero" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Next</span>
  </button>
</div>

Crossfade gallery in a listing card

  • carousel-fade
  • card
  • carousel-indicators
  • object-fit-cover

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

Adding carousel-fade swaps the horizontal slide for an opacity crossfade, and the photos need nothing else because each one covers the slide. The indicators keep the Bootstrap markup but become 24px dot targets through a pseudo-element, and the control icons sit on a small dark circle so they stay visible on bright photos.

View code
<article class="card shadow-sm bcr-stay">
  <div id="bcr-stay" class="carousel slide carousel-fade">
    <div class="carousel-indicators">
      <button type="button" data-bs-target="#bcr-stay" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Photo 1"></button>
      <button type="button" data-bs-target="#bcr-stay" data-bs-slide-to="1" aria-label="Photo 2"></button>
      <button type="button" data-bs-target="#bcr-stay" data-bs-slide-to="2" aria-label="Photo 3"></button>
    </div>
    <div class="carousel-inner">
      <div class="carousel-item active">
        <img src="canal-houses.webp" class="d-block w-100 object-fit-cover bcr-stay-img" width="800" height="600" alt="Gabled canal houses with small boats moored along the water">
      </div>
      <div class="carousel-item">
        <img src="brick-alley.webp" class="d-block w-100 object-fit-cover bcr-stay-img" width="800" height="600" alt="A narrow brick alley with fire escapes and ivy near the apartment" loading="lazy">
      </div>
      <div class="carousel-item">
        <img src="city-skyline.webp" class="d-block w-100 object-fit-cover bcr-stay-img" width="800" height="600" alt="The view of the city skyline and the lake from the roof terrace" loading="lazy">
      </div>
    </div>
    <button class="carousel-control-prev" type="button" data-bs-target="#bcr-stay" data-bs-slide="prev">
      <span class="carousel-control-prev-icon" aria-hidden="true"></span>
      <span class="visually-hidden">Previous photo</span>
    </button>
    <button class="carousel-control-next" type="button" data-bs-target="#bcr-stay" data-bs-slide="next">
      <span class="carousel-control-next-icon" aria-hidden="true"></span>
      <span class="visually-hidden">Next photo</span>
    </button>
  </div>
  <div class="card-body">
    <div class="d-flex justify-content-between align-items-start gap-2">
      <h2 class="h5 card-title mb-1">Canal-side loft, Old Harbor</h2>
      <span class="small text-nowrap"><i class="bi bi-star-fill bcr-stay-star" aria-hidden="true"></i> <span class="visually-hidden">Rated</span> 4.92</span>
    </div>
    <p class="text-body-secondary small mb-3">2 bedrooms, 4 guests, step-free entrance</p>
    <div class="d-flex justify-content-between align-items-center gap-2">
      <p class="mb-0"><strong>148 EUR</strong> <span class="text-body-secondary small">per night</span></p>
      <a href="#" class="btn btn-dark">Check dates</a>
    </div>
  </div>
</article>

Split hero with ratio slides and outside controls

  • ratio-4x3
  • data-bs-slide
  • rounded-4
  • --bs-btn-*

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

A landing page hero with the headline on the left and the slider on the right. Each slide wraps its photo in .ratio.ratio-4x3, so every slide is exactly the same height whatever the source image, and the round buttons below are ordinary buttons with data-bs-target and data-bs-slide. Because they sit inside the .carousel element, the arrow keys work while one of them has focus.

View code
<section class="bcr-split">
  <div class="row g-4 align-items-center">
    <div class="col-md-5">
      <h2 class="display-6 fw-semibold mb-3">Plan the trip, not the spreadsheet</h2>
      <p class="bcr-split-lead mb-4">Waypoint keeps routes, bookings and packing lists on one shared trip board for up to 12 travelers.</p>
      <div class="d-flex flex-wrap gap-2">
        <a href="#" class="btn btn-lg bcr-split-cta">Start free</a>
        <a href="#" class="btn btn-lg btn-outline-dark">See a sample trip</a>
      </div>
    </div>
    <div class="col-md-7">
      <div id="bcr-split" class="carousel slide">
        <div class="carousel-inner rounded-4">
          <div class="carousel-item active">
            <div class="ratio ratio-4x3">
              <img src="highland-road.webp" class="object-fit-cover" width="800" height="600" alt="A narrow road winding through green highland hills under low cloud">
            </div>
            <p class="bcr-split-chip">Day 3: Glen road, 142 km</p>
          </div>
          <div class="carousel-item">
            <div class="ratio ratio-4x3">
              <img src="coast-road.webp" class="object-fit-cover" width="800" height="600" alt="A coastal road cut into wooded cliffs above a calm sea" loading="lazy">
            </div>
            <p class="bcr-split-chip">Day 5: Cliff road, 3 viewpoints saved</p>
          </div>
          <div class="carousel-item">
            <div class="ratio ratio-4x3">
              <img src="snow-camp.webp" class="object-fit-cover" width="800" height="600" alt="Two yellow tents pitched in snow below jagged white peaks" loading="lazy">
            </div>
            <p class="bcr-split-chip">Day 8: Base camp, packing list 34/36</p>
          </div>
        </div>
        <div class="d-flex justify-content-end gap-2 mt-3">
          <button class="btn btn-outline-dark rounded-circle bcr-split-btn" type="button" data-bs-target="#bcr-split" data-bs-slide="prev"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="visually-hidden">Previous slide</span></button>
          <button class="btn btn-outline-dark rounded-circle bcr-split-btn" type="button" data-bs-target="#bcr-split" data-bs-slide="next"><i class="bi bi-arrow-right" aria-hidden="true"></i><span class="visually-hidden">Next slide</span></button>
        </div>
      </div>
    </div>
  </div>
</section>

Text, cards and step-by-step slides

Carousels that hold markup instead of a single photo: reviews, a row of blog cards, an onboarding flow and a site-wide announcement bar.

Testimonial carousel

  • blockquote
  • figure
  • --bs-carousel-indicator-active-bg
  • data-bs-slide

Use the buttons to move between slides.

Text-only slides built from Bootstrap blockquote and figure markup. The indicators are moved below the quote with position: static and recolored through --bs-carousel-indicator-active-bg, and the previous and next buttons sit in the card header, outside the carousel, wired with data-bs-target. A min-height on the slides stops the card from jumping between short and long quotes.

View code
<section class="card border-0 shadow-sm bcr-quote" aria-labelledby="bcr-quote-title">
  <div class="card-body p-4 p-md-5">
    <div class="d-flex justify-content-between align-items-center gap-3 mb-4">
      <h2 class="h5 mb-0" id="bcr-quote-title">What studios say about Ledgerly</h2>
      <div class="d-flex gap-2">
        <button type="button" class="btn bcr-quote-btn bcr-quote-prev" data-bs-target="#bcr-quote" 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 bcr-quote-btn bcr-quote-next" data-bs-target="#bcr-quote" 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="bcr-quote" class="carousel slide">
      <div class="carousel-inner">
        <div class="carousel-item active">
          <figure class="mb-0">
            <blockquote class="blockquote bcr-quote-text">
              <p>We closed the books for March in two days instead of nine. The bank feed matching alone pays for the subscription.</p>
            </blockquote>
            <figcaption class="d-flex align-items-center gap-3">
              <span class="bcr-quote-avatar" aria-hidden="true">MO</span>
              <span><strong class="d-block">Mara Okafor</strong><span class="small text-body-secondary">Finance lead, Fieldnote Studio</span></span>
            </figcaption>
          </figure>
        </div>
        <div class="carousel-item">
          <figure class="mb-0">
            <blockquote class="blockquote bcr-quote-text">
              <p>Our freelancers send invoices from their phones now, and I approve them from mine. Nobody chases PDFs anymore.</p>
            </blockquote>
            <figcaption class="d-flex align-items-center gap-3">
              <span class="bcr-quote-avatar" aria-hidden="true">JL</span>
              <span><strong class="d-block">Jonas Lind</strong><span class="small text-body-secondary">Studio manager, Hale and Birch</span></span>
            </figcaption>
          </figure>
        </div>
        <div class="carousel-item">
          <figure class="mb-0">
            <blockquote class="blockquote bcr-quote-text">
              <p>The VAT report used to be a weekend job. Now it is a button, and our accountant checks it in twenty minutes.</p>
            </blockquote>
            <figcaption class="d-flex align-items-center gap-3">
              <span class="bcr-quote-avatar" aria-hidden="true">PS</span>
              <span><strong class="d-block">Priya Shah</strong><span class="small text-body-secondary">Co-founder, Northlight Audio</span></span>
            </figcaption>
          </figure>
        </div>
      </div>
      <div class="carousel-indicators">
        <button type="button" data-bs-target="#bcr-quote" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Review 1"></button>
        <button type="button" data-bs-target="#bcr-quote" data-bs-slide-to="1" aria-label="Review 2"></button>
        <button type="button" data-bs-target="#bcr-quote" data-bs-slide-to="2" aria-label="Review 3"></button>
      </div>
    </div>
  </div>
</section>

Onboarding steps

  • data-bs-wrap="false"
  • progress
  • slide.bs.carousel
  • prev() / next()

Use the buttons to move between slides.

A four-step welcome flow. data-bs-wrap="false" stops the carousel at the first and last step, and the slide.bs.carousel event updates the progress bar, the step text, the disabled Back button and the Next label in one place. The buttons call prev() and next() on the instance; on the last step Next turns into Get started and shows a status message instead of sliding.

View code
<div class="card border-0 shadow-sm bcr-onb">
  <div id="bcr-onb" class="carousel slide" data-bs-wrap="false">
    <div class="carousel-inner">
      <div class="carousel-item active">
        <div class="bcr-onb-slide">
          <span class="bcr-onb-icon"><i class="bi bi-people" aria-hidden="true"></i></span>
          <h2 class="h4">Invite your crew</h2>
          <p class="mb-0">Add up to 12 people to a trip. Everyone sees the same plan, and nobody needs an account to view it.</p>
        </div>
      </div>
      <div class="carousel-item">
        <div class="bcr-onb-slide">
          <span class="bcr-onb-icon"><i class="bi bi-geo-alt" aria-hidden="true"></i></span>
          <h2 class="h4">Drop pins on the map</h2>
          <p class="mb-0">Paste a link or search for a place. Waypoint groups nearby stops into days for you.</p>
        </div>
      </div>
      <div class="carousel-item">
        <div class="bcr-onb-slide">
          <span class="bcr-onb-icon"><i class="bi bi-wallet2" aria-hidden="true"></i></span>
          <h2 class="h4">Split costs as you go</h2>
          <p class="mb-0">Log a dinner or a ferry ticket once. Every balance updates for the whole group.</p>
        </div>
      </div>
      <div class="carousel-item">
        <div class="bcr-onb-slide">
          <span class="bcr-onb-icon"><i class="bi bi-wifi-off" aria-hidden="true"></i></span>
          <h2 class="h4">Take it offline</h2>
          <p class="mb-0">Download the plan before the trailhead. Your edits sync when you are back in range.</p>
        </div>
      </div>
    </div>
  </div>
  <div class="px-4 pb-4">
    <div class="progress bcr-onb-progress mb-3" role="progressbar" aria-label="Setup progress" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">
      <div class="progress-bar" style="width: 25%"></div>
    </div>
    <div class="d-flex justify-content-between align-items-center gap-2">
      <button type="button" class="btn btn-link bcr-onb-back" disabled>Back</button>
      <span class="small text-body-secondary bcr-onb-step" aria-live="polite">Step 1 of 4</span>
      <button type="button" class="btn px-4 bcr-onb-next">Next</button>
    </div>
    <p class="small mt-3 mb-0 d-none bcr-onb-done" role="status">You are all set. Your first trip board is ready.</p>
  </div>
</div>

Announcement bar

  • carousel-fade
  • carousel-control-*
  • bi-chevron-*
  • filter: none

Use the buttons to move between slides.

A slim store banner above the header, made from a crossfading carousel with text slides. Text-only slides need their own background for a clean crossfade, as the Bootstrap docs point out, so every item gets the bar color. The controls are 44px wide, use icon-font chevrons, and set filter: none so a dark-mode page cannot invert them on this fixed dark green.

View code
<div class="bcr-bar">
  <div id="bcr-bar" class="carousel slide carousel-fade">
    <div class="carousel-inner">
      <div class="carousel-item active">
        <p class="mb-0">Free shipping on orders over 60 EUR. <a href="#">Shop the new season</a></p>
      </div>
      <div class="carousel-item">
        <p class="mb-0">We repair any jacket we sold, free, for life. <a href="#">How repairs work</a></p>
      </div>
      <div class="carousel-item">
        <p class="mb-0">Store pickup is now open in 14 cities. <a href="#">Find a store</a></p>
      </div>
    </div>
    <button class="carousel-control-prev" type="button" data-bs-target="#bcr-bar" data-bs-slide="prev"><i class="bi bi-chevron-left" aria-hidden="true"></i><span class="visually-hidden">Previous announcement</span></button>
    <button class="carousel-control-next" type="button" data-bs-target="#bcr-bar" data-bs-slide="next"><i class="bi bi-chevron-right" aria-hidden="true"></i><span class="visually-hidden">Next announcement</span></button>
  </div>
</div>
<header class="bcr-bar-head d-flex align-items-center justify-content-between gap-3">
  <a href="#" class="bcr-bar-brand">Fernhill</a>
  <nav aria-label="Main">
    <ul class="nav d-none d-sm-flex">
      <li class="nav-item"><a class="nav-link" href="#">Women</a></li>
      <li class="nav-item"><a class="nav-link" href="#">Men</a></li>
      <li class="nav-item"><a class="nav-link" href="#">Gear</a></li>
    </ul>
  </nav>
  <a href="#" class="btn btn-outline-dark bcr-bar-bag"><i class="bi bi-bag" aria-hidden="true"></i><span class="visually-hidden">Bag, 2 items</span></a>
</header>
<div class="bcr-bar-page">
  <h2 class="h3 mb-2">Autumn layers are in</h2>
  <p class="text-body-secondary mb-0">Wool, waxed cotton and recycled down, made to be repaired.</p>
</div>

Autoplay, timing and the JavaScript API

Autoplay done the accessible way, with a visible pause button and no rotation under reduced motion, plus per-slide intervals, a live slide counter and the methods and events in action.

Autoplay with a pause button

  • cycle()
  • pause()
  • aria-roledescription
  • prefers-reduced-motion

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

The pattern WCAG 2.2.2 asks for: an autoplaying carousel with a visible pause control that comes first in the tab order. The instance is created with pause: false and the script handles hover, keyboard focus and the button itself through cycle() and pause(), so a mouseleave can never restart a carousel the user stopped. Slides carry the ARIA carousel roles from the APG pattern, aria-live switches to polite while paused, and with reduced motion on it never starts by itself.

View code
<section id="bcr-auto" class="carousel slide bcr-auto" aria-roledescription="carousel" aria-label="Current exhibitions">
  <div class="d-flex align-items-center gap-2 mb-2">
    <button type="button" class="btn btn-dark bcr-auto-toggle"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i><span class="bcr-auto-label">Pause slideshow</span></button>
    <button type="button" class="btn btn-outline-dark bcr-auto-nav ms-auto" data-bs-target="#bcr-auto" data-bs-slide="prev"><i class="bi bi-chevron-left" aria-hidden="true"></i><span class="visually-hidden">Previous slide</span></button>
    <button type="button" class="btn btn-outline-dark bcr-auto-nav" data-bs-target="#bcr-auto" data-bs-slide="next"><i class="bi bi-chevron-right" aria-hidden="true"></i><span class="visually-hidden">Next slide</span></button>
  </div>
  <div class="carousel-inner rounded-3" aria-live="off" aria-atomic="false">
    <div class="carousel-item active" role="group" aria-roledescription="slide" aria-label="1 of 3">
      <img src="jellyfish.webp" class="d-block w-100 object-fit-cover bcr-auto-img" width="800" height="600" alt="An orange sea nettle jellyfish trailing long tentacles in deep blue water">
      <div class="bcr-auto-cap">
        <h2 class="h5 mb-1">Drifters: life without a skeleton</h2>
        <p class="small mb-0">Hall B, until 12 January</p>
      </div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="2 of 3">
      <img src="fjord.webp" class="d-block w-100 object-fit-cover bcr-auto-img" width="800" height="600" alt="A deep blue fjord between granite cliffs" loading="lazy">
      <div class="bcr-auto-cap">
        <h2 class="h5 mb-1">Cold water, deep time</h2>
        <p class="small mb-0">Hall A, opens 4 November</p>
      </div>
    </div>
    <div class="carousel-item" role="group" aria-roledescription="slide" aria-label="3 of 3">
      <img src="coast-road.webp" class="d-block w-100 object-fit-cover bcr-auto-img" width="800" height="600" alt="Wooded sea cliffs above a calm blue sea" loading="lazy">
      <div class="bcr-auto-cap">
        <h2 class="h5 mb-1">Kelp coast: forests under the cliffs</h2>
        <p class="small mb-0">Rooftop gallery, weekends only</p>
      </div>
    </div>
  </div>
</section>

Per-slide intervals with a progress line

  • data-bs-interval
  • carousel-fade
  • slide.bs.carousel
  • CSS custom property

Each .carousel-item has its own data-bs-interval (8 seconds for the main offer, 4 for the others), which Bootstrap reads every time a slide starts. On slide.bs.carousel the script copies the incoming value into --bcr-promo-dur, and a CSS animation fills the progress line over exactly that time. The animation only exists inside a no-preference media query, the pause button stops both, and under reduced motion the banner never starts rotating.

View code
<div class="bcr-promo">
  <section id="bcr-promo" class="carousel slide carousel-fade" aria-roledescription="carousel" aria-label="Current offers">
    <div class="carousel-inner" aria-live="off" aria-atomic="false">
      <div class="carousel-item active" data-bs-interval="8000" role="group" aria-roledescription="slide" aria-label="1 of 3">
        <div class="bcr-promo-slide bcr-promo-a">
          <div class="bcr-promo-text">
            <h2 class="h3 mb-2">Tents up to 40% off</h2>
            <p class="mb-3">Last season two-person and family tents, while sizes last.</p>
            <a href="#" class="btn btn-light">Shop tents</a>
          </div>
          <img src="snow-camp.webp" class="bcr-promo-img" width="800" height="600" alt="Two yellow tents pitched in snow below jagged peaks">
        </div>
      </div>
      <div class="carousel-item" data-bs-interval="4000" role="group" aria-roledescription="slide" aria-label="2 of 3">
        <div class="bcr-promo-slide bcr-promo-b">
          <div class="bcr-promo-text">
            <h2 class="h3 mb-2">Free repairs, any year</h2>
            <p class="mb-3">Torn strap or broken zip? Send it in and we fix it.</p>
            <a href="#" class="btn btn-light">Start a repair</a>
          </div>
          <img src="mountain-hiker.webp" class="bcr-promo-img" width="800" height="600" alt="A hiker with a large backpack above a misty valley" loading="lazy">
        </div>
      </div>
      <div class="carousel-item" data-bs-interval="4000" role="group" aria-roledescription="slide" aria-label="3 of 3">
        <div class="bcr-promo-slide bcr-promo-c">
          <div class="bcr-promo-text">
            <h2 class="h3 mb-2">Trail maps, now offline</h2>
            <p class="mb-3">Every map in the app works without signal from today.</p>
            <a href="#" class="btn btn-light">Get the app</a>
          </div>
          <img src="forest-waterfall.webp" class="bcr-promo-img" width="800" height="600" alt="A waterfall falling into a deep green forest gorge" loading="lazy">
        </div>
      </div>
    </div>
  </section>
  <div class="d-flex align-items-center gap-3 mt-3">
    <button type="button" class="btn btn-outline-dark bcr-promo-toggle"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i><span class="bcr-promo-label">Pause offers</span></button>
    <div class="bcr-promo-track flex-grow-1" aria-hidden="true"><div class="bcr-promo-fill"></div></div>
    <span class="small text-body-secondary bcr-promo-time">8 s</span>
  </div>
</div>

Photo essay with a slide counter

  • slid.bs.carousel
  • event.to
  • data-bs-theme="light"
  • keyboard

Focus an arrow, then use the arrow keys.

A slid.bs.carousel listener writes event.to + 1 into the counter and copies the slide caption into the figcaption with textContent. On this dark page the carousel carries data-bs-theme="light": in Bootstrap 5.3 a dark color mode also turns the carousel controls and indicators black, which disappears on dark photos. Focus a control and the left and right arrow keys move the slides, because Bootstrap listens for keydown on the .carousel element.

View code
<figure class="bcr-count mb-0">
  <div id="bcr-count" class="carousel slide" data-bs-theme="light">
    <div class="carousel-inner rounded-3">
      <div class="carousel-item active" data-bcr-caption="The coast road: 40 km of switchbacks with the sea on one side the whole way.">
        <img src="coast-road.webp" class="d-block w-100 object-fit-cover bcr-count-img" width="800" height="600" alt="A road cut into wooded sea cliffs above a calm blue sea">
      </div>
      <div class="carousel-item" data-bcr-caption="The pass road: single track, passing places every few hundred meters, and sheep that do not move.">
        <img src="highland-road.webp" class="d-block w-100 object-fit-cover bcr-count-img" width="800" height="600" alt="A narrow road climbing through green highland hills under low cloud" loading="lazy">
      </div>
      <div class="carousel-item" data-bcr-caption="The canyon road: we timed the last stretch for sunset and almost missed it.">
        <img src="red-rock-canyon.webp" class="d-block w-100 object-fit-cover bcr-count-img" width="800" height="600" alt="Red rock cliffs lit orange by the setting sun" loading="lazy">
      </div>
      <div class="carousel-item" data-bcr-caption="The fjord road ends at a ferry. The view from the ledge above it is the reason to go.">
        <img src="fjord.webp" class="d-block w-100 object-fit-cover bcr-count-img" width="800" height="600" alt="A blue fjord seen from a high granite ledge" loading="lazy">
      </div>
      <div class="carousel-item" data-bcr-caption="The last road was no road at all: two days on foot to the snow camp.">
        <img src="snow-camp.webp" class="d-block w-100 object-fit-cover bcr-count-img" width="800" height="600" alt="Yellow tents in a snowfield below white peaks" loading="lazy">
      </div>
    </div>
    <p class="bcr-count-badge" aria-live="polite"><span class="visually-hidden">Photo </span><span class="bcr-count-now">1</span><span aria-hidden="true"> / </span><span class="visually-hidden"> of </span>5</p>
    <button class="carousel-control-prev" type="button" data-bs-target="#bcr-count" data-bs-slide="prev">
      <span class="carousel-control-prev-icon" aria-hidden="true"></span>
      <span class="visually-hidden">Previous photo</span>
    </button>
    <button class="carousel-control-next" type="button" data-bs-target="#bcr-count" data-bs-slide="next">
      <span class="carousel-control-next-icon" aria-hidden="true"></span>
      <span class="visually-hidden">Next photo</span>
    </button>
  </div>
  <figcaption class="bcr-count-cap mt-3">The coast road: 40 km of switchbacks with the sea on one side the whole way.</figcaption>
</figure>

Methods and events, logged live

  • next() / prev() / to()
  • slide.bs.carousel
  • slid.bs.carousel
  • { once: true }

Press a button and watch the event log.

Every button calls a method on the instance and both carousel events are logged with their from, to and direction properties. The chain button shows the documented pattern for back-to-back moves: start with to(1) and run to(3) from a one-time slid.bs.carousel listener, because a method call made during a transition is not run right away. The log entries are built with createElement and textContent.

View code
<div class="card border-0 shadow-sm bcr-api">
  <div class="row g-0">
    <div class="col-md-6 p-3">
      <div id="bcr-api" class="carousel slide">
        <div class="carousel-inner rounded-3">
          <div class="carousel-item active">
            <img src="granite-valley.webp" class="d-block w-100 object-fit-cover bcr-api-img" width="800" height="600" alt="Slide 0: granite cliffs above a river">
          </div>
          <div class="carousel-item">
            <img src="canal-houses.webp" class="d-block w-100 object-fit-cover bcr-api-img" width="800" height="600" alt="Slide 1: canal houses and boats" loading="lazy">
          </div>
          <div class="carousel-item">
            <img src="jellyfish.webp" class="d-block w-100 object-fit-cover bcr-api-img" width="800" height="600" alt="Slide 2: an orange jellyfish" loading="lazy">
          </div>
          <div class="carousel-item">
            <img src="highland-road.webp" class="d-block w-100 object-fit-cover bcr-api-img" width="800" height="600" alt="Slide 3: a road through green hills" loading="lazy">
          </div>
        </div>
      </div>
      <div class="d-flex flex-wrap gap-2 mt-3">
        <button type="button" class="btn btn-outline-primary font-monospace bcr-api-prev">prev()</button>
        <button type="button" class="btn btn-outline-primary font-monospace bcr-api-next">next()</button>
        <button type="button" class="btn btn-outline-primary font-monospace bcr-api-to">to(2)</button>
        <button type="button" class="btn btn-primary font-monospace bcr-api-chain">to(1), then to(3)</button>
      </div>
    </div>
    <div class="col-md-6 p-3 bcr-api-side">
      <h2 class="h6 mb-2">Event log</h2>
      <p class="small text-body-secondary mb-0 bcr-api-empty">Press a button to see slide and slid fire.</p>
      <ol class="list-unstyled font-monospace small mb-0 bcr-api-log" role="log"></ol>
    </div>
  </div>
</div>

Dark variant, theming and custom transitions

The data-bs-theme replacement for the deprecated carousel-dark class, the three carousel CSS variables, indicators restyled as thumbnails and a vertical slide made with two transform overrides.

Dark variant with data-bs-theme

  • data-bs-theme="dark"
  • carousel-indicators
  • carousel-control-*
  • explicit slide colors

Use the arrows or the indicators.

The carousel-dark class is deprecated since Bootstrap 5.3.0; data-bs-theme="dark" on the carousel gives the same black controls, indicators and captions for light slides. The catch: that attribute also switches every Bootstrap color token inside the carousel to dark mode, so these cream slides set their own background, text and link colors instead of relying on bg-body or the default link color.

View code
<div id="bcr-dv" class="carousel slide bcr-dv" data-bs-theme="dark">
  <div class="carousel-indicators">
    <button type="button" data-bs-target="#bcr-dv" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
    <button type="button" data-bs-target="#bcr-dv" data-bs-slide-to="1" aria-label="Slide 2"></button>
    <button type="button" data-bs-target="#bcr-dv" data-bs-slide-to="2" aria-label="Slide 3"></button>
  </div>
  <div class="carousel-inner">
    <div class="carousel-item active">
      <div class="bcr-dv-slide">
        <img src="granite-valley.webp" class="bcr-dv-img" width="800" height="600" alt="Granite cliffs rising above pine forest and a quiet river">
        <div>
          <h2 class="h4">Granite valley in spring</h2>
          <p>Meltwater fills the river by May, and the meadows stay quiet until the summer buses arrive.</p>
          <a href="#">Read the field notes</a>
        </div>
      </div>
    </div>
    <div class="carousel-item">
      <div class="bcr-dv-slide">
        <img src="forest-waterfall.webp" class="bcr-dv-img" width="800" height="600" alt="A waterfall dropping into a green forest gorge" loading="lazy">
        <div>
          <h2 class="h4">The falls after rain</h2>
          <p>Go the morning after a storm. The spray reaches the upper viewpoint and the trail is nearly empty.</p>
          <a href="#">Read the field notes</a>
        </div>
      </div>
    </div>
    <div class="carousel-item">
      <div class="bcr-dv-slide">
        <img src="highland-road.webp" class="bcr-dv-img" width="800" height="600" alt="A road winding through green hills under low cloud" loading="lazy">
        <div>
          <h2 class="h4">Cloud on the pass</h2>
          <p>Low cloud lifts by noon most days. Start late, park at the second bend and walk the last mile.</p>
          <a href="#">Read the field notes</a>
        </div>
      </div>
    </div>
  </div>
  <button class="carousel-control-prev" type="button" data-bs-target="#bcr-dv" 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="#bcr-dv" data-bs-slide="next">
    <span class="carousel-control-next-icon" aria-hidden="true"></span>
    <span class="visually-hidden">Next</span>
  </button>
</div>

Themed with the carousel CSS variables

  • --bs-carousel-indicator-active-bg
  • --bs-carousel-caption-color
  • data-bs-slide
  • carousel-indicators

Use the arrows or the indicators.

Bootstrap 5.3 exposes only three carousel CSS variables: the indicator color, the caption color and the control icon filter. This demo sets the first two to amber and cream on its own class, then moves the indicators and two plain buttons into a brown control bar under the photo. Plain buttons work because the data API listens for data-bs-slide on any element, not only on .carousel-control-prev and .carousel-control-next.

View code
<div id="bcr-theme" class="carousel slide bcr-theme">
  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="red-rock-canyon.webp" class="d-block w-100 object-fit-cover bcr-theme-img" width="800" height="600" alt="Red sandstone cliffs glowing orange at sunset">
      <div class="carousel-caption bcr-theme-cap">
        <h2 class="h4 mb-1">Canyon rim at golden hour</h2>
        <p class="mb-0 d-none d-md-block">Guided sunset walks, Thursday to Sunday.</p>
      </div>
    </div>
    <div class="carousel-item">
      <img src="mountain-hiker.webp" class="d-block w-100 object-fit-cover bcr-theme-img" width="800" height="600" alt="A hiker with a big pack looking across hazy mountains" loading="lazy">
      <div class="carousel-caption bcr-theme-cap">
        <h2 class="h4 mb-1">Ridge days from 95 EUR</h2>
        <p class="mb-0 d-none d-md-block">Seven hours, one guide for every six walkers.</p>
      </div>
    </div>
    <div class="carousel-item">
      <img src="coast-road.webp" class="d-block w-100 object-fit-cover bcr-theme-img" width="800" height="600" alt="A coastal road on wooded cliffs above the sea" loading="lazy">
      <div class="carousel-caption bcr-theme-cap">
        <h2 class="h4 mb-1">Cliff path by e-bike</h2>
        <p class="mb-0 d-none d-md-block">Bikes, helmets and the picnic are on us.</p>
      </div>
    </div>
  </div>
  <div class="bcr-theme-bar">
    <div class="carousel-indicators">
      <button type="button" data-bs-target="#bcr-theme" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
      <button type="button" data-bs-target="#bcr-theme" data-bs-slide-to="1" aria-label="Slide 2"></button>
      <button type="button" data-bs-target="#bcr-theme" data-bs-slide-to="2" aria-label="Slide 3"></button>
    </div>
    <div class="d-flex gap-2">
      <button type="button" class="bcr-theme-btn" data-bs-target="#bcr-theme" data-bs-slide="prev"><i class="bi bi-arrow-left" aria-hidden="true"></i><span class="visually-hidden">Previous</span></button>
      <button type="button" class="bcr-theme-btn bcr-theme-next" data-bs-target="#bcr-theme" data-bs-slide="next"><i class="bi bi-arrow-right" aria-hidden="true"></i><span class="visually-hidden">Next</span></button>
    </div>
  </div>
</div>

Thumbnail indicators, no extra JavaScript

  • carousel-indicators
  • data-bs-slide-to
  • aria-current
  • CSS grid

Use the arrows or the indicators.

The thumbnails are the real .carousel-indicators buttons with an image inside, moved under the photo with position: static and laid out as a grid. Because they stay inside the .carousel element, Bootstrap moves the active class and aria-current for you, so no sync script is needed. The only JavaScript is the instance creation that turns on swiping.

View code
<div id="bcr-ti" class="carousel slide bcr-ti">
  <div class="position-relative">
    <div class="carousel-inner rounded-3">
      <div class="carousel-item active">
        <img src="forest-waterfall.webp" class="d-block w-100 object-fit-cover bcr-ti-img" width="800" height="600" alt="A tall waterfall falling into a green forested gorge">
      </div>
      <div class="carousel-item">
        <img src="granite-valley.webp" class="d-block w-100 object-fit-cover bcr-ti-img" width="800" height="600" alt="Granite cliffs above pine trees and a calm river" loading="lazy">
      </div>
      <div class="carousel-item">
        <img src="snow-camp.webp" class="d-block w-100 object-fit-cover bcr-ti-img" width="800" height="600" alt="Yellow tents in deep snow below white peaks" loading="lazy">
      </div>
      <div class="carousel-item">
        <img src="canal-houses.webp" class="d-block w-100 object-fit-cover bcr-ti-img" width="800" height="600" alt="Colorful canal houses with boats on the water" loading="lazy">
      </div>
      <div class="carousel-item">
        <img src="jellyfish.webp" class="d-block w-100 object-fit-cover bcr-ti-img" width="800" height="600" alt="An orange jellyfish drifting in deep blue water" loading="lazy">
      </div>
    </div>
    <button class="carousel-control-prev" type="button" data-bs-target="#bcr-ti" 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="#bcr-ti" data-bs-slide="next">
      <span class="carousel-control-next-icon" aria-hidden="true"></span>
      <span class="visually-hidden">Next</span>
    </button>
  </div>
  <div class="carousel-indicators bcr-ti-thumbs">
    <button type="button" data-bs-target="#bcr-ti" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1: waterfall"><img src="forest-waterfall.webp" alt="" width="800" height="600"></button>
    <button type="button" data-bs-target="#bcr-ti" data-bs-slide-to="1" aria-label="Slide 2: granite valley"><img src="granite-valley.webp" alt="" width="800" height="600"></button>
    <button type="button" data-bs-target="#bcr-ti" data-bs-slide-to="2" aria-label="Slide 3: snow camp"><img src="snow-camp.webp" alt="" width="800" height="600"></button>
    <button type="button" data-bs-target="#bcr-ti" data-bs-slide-to="3" aria-label="Slide 4: canal houses"><img src="canal-houses.webp" alt="" width="800" height="600"></button>
    <button type="button" data-bs-target="#bcr-ti" data-bs-slide-to="4" aria-label="Slide 5: jellyfish"><img src="jellyfish.webp" alt="" width="800" height="600"></button>
  </div>
</div>

Vertical slide transition

  • carousel-item-next / -prev
  • carousel-item-start / -end
  • data-bs-touch="false"
  • transition-duration

Use the arrows or the indicators.

Bootstrap animates a slide by translating .carousel-item-next, .carousel-item-prev and the outgoing .active item along the X axis. Overriding those four selectors with translateY, scoped to one class, turns it into a vertical ticker for release notes; the duration change sits in a no-preference media query and Bootstrap already removes the transition under reduced motion. Swiping is horizontal only, so data-bs-touch="false" turns it off here.

View code
<section class="card shadow-sm bcr-vert" aria-labelledby="bcr-vert-title">
  <div class="card-body d-flex align-items-center gap-3">
    <span class="bcr-vert-icon"><i class="bi bi-megaphone" aria-hidden="true"></i></span>
    <div class="flex-grow-1 min-w-0">
      <h2 class="visually-hidden" id="bcr-vert-title">What is new in Waypoint</h2>
      <div id="bcr-vert" class="carousel slide" data-bs-touch="false">
        <div class="carousel-inner">
          <div class="carousel-item active">
            <h3 class="h6 mb-1">Offline maps for every trip</h3>
            <p class="small text-body-secondary mb-0">Version 4.2. Download a region once and it stays on the phone.</p>
          </div>
          <div class="carousel-item">
            <h3 class="h6 mb-1">Split costs in any currency</h3>
            <p class="small text-body-secondary mb-0">Version 4.1. Rates update daily from the central bank feed.</p>
          </div>
          <div class="carousel-item">
            <h3 class="h6 mb-1">Shared packing lists</h3>
            <p class="small text-body-secondary mb-0">Version 4.0. Tick an item and the whole group sees it.</p>
          </div>
        </div>
      </div>
    </div>
    <div class="d-flex flex-column gap-1">
      <button type="button" class="btn btn-light bcr-vert-btn" data-bs-target="#bcr-vert" data-bs-slide="prev"><i class="bi bi-chevron-up" aria-hidden="true"></i><span class="visually-hidden">Previous update</span></button>
      <button type="button" class="btn btn-light bcr-vert-btn bcr-vert-next" data-bs-target="#bcr-vert" data-bs-slide="next"><i class="bi bi-chevron-down" aria-hidden="true"></i><span class="visually-hidden">Next update</span></button>
    </div>
  </div>
</section>

What is the Bootstrap carousel and what does it need to run?

The Bootstrap framework holds a 74.8% share among sites with a known CSS framework, according to W3Techs (October 2026).

That reach is why carousel snippets are everywhere. It is also why outdated ones keep ranking, and several top tutorials still show Bootstrap 3.4.1 and 4.x markup.

The component itself is a slideshow for images, text, or custom markup. People mostly use it for hero banners and product galleries, plus the occasional testimonial rotator.

The previous and next controls and the indicators are built in, but neither is required.

On Bootstrap 5.3 you load the CSS file plus either bootstrap.js or bootstrap.min.js, never both. There is no jQuery. A CDN such as jsDelivr works fine, or you can install the bootstrap package with npm. Bootstrap 4.6 still initializes through jQuery, and Bootstrap 4 has reached end of life (Bootstrap 4.6 docs).

Setup is manual with new bootstrap.Carousel() unless the carousel autoplays. One marked data-bs-ride="carousel" starts by itself on page load.

Popper isn’t involved. It belongs to tooltips and popovers, along with dropdowns (Bootstrap docs).

The sliding itself is CSS. Transforms do the motion, and JavaScript handles timing, controls, and events.

The limits are short. Nested carousels aren’t supported and only one slide shows at a time. Slide dimensions aren’t normalized either, so that part is on you. The Bootstrap docs also warn that carousels in general can cause usability and accessibility problems.

How to build a Bootstrap carousel step by step

A working carousel is a .carousel wrapper with a unique id, a .carousel-inner container, slides with .active on one of them, and the Bootstrap JavaScript file. Controls, indicators, and captions are extras.

  1. Load the Bootstrap CSS in the head and the JavaScript file before the closing body tag.
  2. Create a div with the classes carousel and slide and give it a unique id.
  3. Add a .carousel-inner div inside it.
  4. Add one .carousel-item per slide and put .active on the first.
  5. Place an image with d-block w-100 and real alt text in each slide.
  6. Add previous/next buttons and indicators that point to the carousel id.
  7. Add data-bs-ride="carousel" only if the carousel should autoplay on load.

Base markup

This is the smallest carousel that renders on Bootstrap 5.3.

<div id="heroCarousel" class="carousel slide">
  <div class="carousel-inner">
    <div class="carousel-item active">
      <img src="slide-1.jpg" class="d-block w-100" alt="Describe slide one">
    </div>
    <div class="carousel-item">
      <img src="slide-2.jpg" class="d-block w-100" alt="Describe slide two">
    </div>
  </div>
</div>

The docs state it plainly: without .active on one slide, the carousel is not visible.

The d-block and w-100 classes stop the browser’s default inline image alignment. The whole component is plain HTML plus data attributes, so there is nothing to compile.

Previous and next controls

Each control is a button with the class carousel-control-prev or carousel-control-next. The docs recommend buttons, though anchors with role="button" work too.

Point it at the carousel with data-bs-target, which holds the carousel id, such as #heroCarousel (links use href instead). data-bs-slide takes prev or next. Screen readers need a visually-hidden span reading Previous or Next, and the icon span gets aria-hidden="true".

If the target and the id do not match, the buttons do nothing. With several carousels on one page, every id has to be unique, which is easy to forget when you duplicate a block.

Indicators

Indicators let users jump straight to a slide. Wrap them in a div with the class carousel-indicators and use one button per slide, each with data-bs-slide-to set to the slide index, starting at 0. The first button gets active and aria-current="true", and every button gets an aria-label such as “Slide 1”.

Count the buttons against the slides. A third button on a two-slide carousel points nowhere.

Captions

A caption is a div with the class carousel-caption, placed inside the .carousel-item it belongs to.

Bootstrap’s own example hides captions on small screens with d-none and brings them back from the medium breakpoint with d-md-block.

Keep a caption to a heading and one line. Anything longer covers the picture on a phone.

How do you stop, start, or change carousel autoplay?

The ride option controls autoplay, and in markup it is data-bs-ride. Use carousel to start on page load, or true to start after the first manual slide change. The pause between slides is the interval option, 5000 ms unless you change it.

OptionDefaultEffect
ridefalse“carousel” autoplays on load, true autoplays after the first manual change
interval5000Milliseconds between automatic slide changes
pause“hover”Stops on mouseenter, resumes on mouseleave
wraptrueLoops continuously, or stops at the first and last slide when false

Stop autoplay

In markup, remove data-bs-ride from the .carousel element, since the default is false. In a script, pause() stops a running carousel and cycle() starts it again.

Versions disagree on one detail. The Bootstrap 4.6 docs name interval set to false as an off switch, but the 5.3 options table lists interval as a number only, so on 5.3 remove ride instead.

Change the speed

The default delay is 5000 ms, or 5 seconds.

Set a carousel-wide value with data-bs-interval on the .carousel element, or pass interval: 2000 to the constructor. To override one slide, put data-bs-interval on its .carousel-item.

Bootstrap’s demo gives the first slide 10000 and the second 2000, and the third keeps the carousel-wide value.

Pause, touch, and keyboard behavior

Plenty happens in the background here, and most of it is on by default. With pause set to “hover”, touch devices pause on touchend for two intervals before resuming. Browsers with the Page Visibility API stop the cycle when the tab is inactive or the window is minimized. touch: false turns off swiping, and keyboard: false stops the carousel from reacting to key presses.

Autoplay is a usability decision as much as a setting. The Bootstrap docs recommend avoiding autoplaying carousels, and adding an explicit pause control when you keep one. I’d leave it off unless there’s a reason.

How do you change the transition and style of a carousel?

A crossfade comes from .carousel-fade, and dark controls and captions come from data-bs-theme="dark". Speed is $carousel-transition-duration, 0.6 seconds by default. Slide is the default transition.

Switch to a crossfade

By default slides move horizontally. Add carousel-fade to the .carousel and they blend in place instead.

Text-only slides need a solid background for the crossfade to look right. Bootstrap suggests .bg-body on each .carousel-item, or custom CSS.

Change the slide speed

Speed is a Sass variable, not a data attribute.

$carousel-transition-duration defaults to .6s as of Bootstrap 5.3.8. Override it before compiling, or write custom CSS on .carousel-item if you use the compiled file.

Stack a second transition and order matters: define the transform first, as in transition: transform 2s ease, opacity .5s ease-out (Bootstrap docs).

If nothing animates during testing, check the operating system’s reduced motion setting. Bootstrap disables the sliding animation when prefers-reduced-motion is active.

Dark variant and Sass variables

.carousel-dark was deprecated in v5.3.0. Set data-bs-theme="dark" on the root element, a wrapper, or the carousel itself. The $carousel-dark-* variables followed in v5.3.4.

Sizing has its own variables. $carousel-control-width is 15%, $carousel-indicator-width is 30px, $carousel-indicator-height is 3px, and $carousel-caption-width is 70%.

Caption text sits on photos, so check color contrast against the brightest part of each image. Bootstrap’s accessibility docs cite the WCAG 2.2 text ratio of 4.5:1 and warn that some default palette combinations can fall below it.

How do you size carousel images and keep them fast?

Bootstrap does not normalize slide dimensions, so sizing is left to you. Crop every image to one aspect ratio, give each d-block and w-100, and keep the first slide out of any lazy loading.

Keep slide heights equal

The simplest fix is to export all slides at one ratio, 16:9 for example. If that isn’t possible, set a fixed height on the images in CSS and add Bootstrap’s object-fit-cover utility.

Because w-100 stretches the image to its container, it suits a responsive design. It does not shrink the file, so serve smaller versions to small screens with srcset and sizes.

Also add width and height attributes, or a CSS aspect-ratio, so the browser reserves space before the image loads. On mobile, 62% of pages leave at least one image without explicit dimensions (HTTP Archive Web Almanac 2025).

Load the first slide first

About 16% to 17% of pages lazy-load their Largest Contentful Paint image, a share that has held steady since 2024 (HTTP Archive Web Almanac 2025).

A carousel at the top of a page makes that slip easy. Slide one becomes the page’s hero image, and a blanket loading="lazy" on every slide delays it.

Chrome for Developers (October 2025) gives the general rule: keep the LCP image discoverable in the HTML, add fetchpriority="high", and avoid loading="lazy" on it.

For a carousel, that means slide one gets fetchpriority="high" and no loading="lazy". Later slides can be lazy. Slide two is the exception when the interval is short, since a 2000 ms delay leaves little time to load, so keep it eager too.

Bootstrap’s docs cover neither loading attribute, so this rule comes from combining Chrome’s LCP guidance with the carousel showing one slide at a time.

How do you control the carousel with JavaScript?

Create an instance with new bootstrap.Carousel(element, options), then call next(), prev(), or to(index) on it. The events slide.bs.carousel and slid.bs.carousel report what the carousel is doing.

Create and retrieve an instance

The constructor takes a DOM element or a CSS selector, plus an optional options object such as { interval: 2000, touch: false }.

Everything in this section is plain JavaScript on Bootstrap 5.3. getInstance(element) returns the existing instance, or null if there isn’t one. getOrCreateInstance(element, options) returns the existing instance or builds one. dispose() removes the stored data from the element. bootstrap.Carousel.Default holds the default options, and a change there applies to every carousel created afterward.

Prefer getOrCreateInstance in shared scripts. For a carousel Bootstrap already initialized on load, it hands back that instance instead of building a second one.

Methods

MethodWhat it does
nextMoves to the next slide
prevMoves to the previous slide
to(index)Jumps to a slide by zero-based index
nextWhenVisibleMoves on only when the page, the carousel, and its parent are visible

pause() and cycle() belong to the same set.

Asynchronous calls

Every method returns before its transition ends.

The docs add that a method call on a transitioning carousel is ignored, and their example chains two to() calls by running the second one from a slid.bs.carousel listener. In the 5.3.8 source, next() and prev() are dropped mid-transition, while to() waits for the current slide to finish and then runs.

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

el.addEventListener('slid.bs.carousel', () => carousel.to(2), { once: true })

carousel.to(1)

The { once: true } flag stops the listener from firing after every later slide.

Events

Both events fire on the .carousel element itself. slide.bs.carousel fires the moment a slide change starts, and slid.bs.carousel fires when it finishes.

Each one carries a direction of “left” or “right” and a relatedTarget, the slide element moving into place. The from and to properties hold the indexes of the current slide and the next one.

Together with the methods, these events make up the carousel’s programmatic API. Use them to sync a slide counter or prepare content for the incoming slide.

One catch with jQuery. If it is on the page and the body has no data-bs-no-jquery attribute, Bootstrap emits events through jQuery’s event system, so listen with .on() and not addEventListener (Bootstrap 5.3 docs).

Is the Bootstrap carousel accessible?

Not from the default markup. An autoplaying Bootstrap carousel lacks the rotation control, focus-based pausing, and container and slide semantics that the W3C carousel pattern calls for.

It can get there. Bootstrap’s accessibility page says WCAG 2.2 compliance depends on the author’s markup and scripting, and the carousel needs both.

What WCAG 2.2 requires

Success Criterion 2.2.2, Pause, Stop, Hide, sits at Level A (W3C Understanding document, updated August 2026). Auto-updating content that starts on its own and appears beside other content needs a way to pause it, stop it, hide it, or control how often it updates.

The Understanding document counts auto-advancing presentations as auto-updating content, and auto-updating content gets no five-second exception. A carousel with autoplay on is in scope however long its interval is.

Hover-pausing does not qualify as a mechanism. The document rejects an animation that stops only while the user has focus on it, and hover-only pausing has the same weakness.

Level A is the entry point of web accessibility conformance, so autoplay without a control fails at the lowest level.

What the W3C carousel pattern requires

The ARIA Authoring Practices Guide lays out what an accessible autoplaying carousel looks like. Bootstrap’s 5.3 docs example covers none of it, as the table shows.

RequirementBootstrap 5.3 docs exampleWhat you add
Rotation control, first in the tab orderNone built inA button that calls pause() and cycle(), labeled like the APG example, “Stop automatic slide show” or “Start automatic slide show”
Stop when keyboard focus entersHover and touch pause onlyA focusin listener that calls pause()
Carousel container semanticsNo role or labelrole=”region” (or role=”group”), aria-roledescription=”carousel”, aria-label
Slide semanticsNonerole=”group”, aria-roledescription=”slide”, a label such as “3 of 10”
Live region on the slide wrapper (optional in the pattern)Nonearia-live “off” while rotating, “polite” when stopped

Those roles and properties are ARIA. Bootstrap’s accessibility page expects authors to add further ARIA roles and attributes where a component needs them.

What Bootstrap’s own docs say

The Bootstrap 4.6 docs call carousels generally non-compliant with accessibility standards. The 5.3 docs soften that to carousels often causing usability and accessibility challenges, with a pointer to WCAG 2.2.2 and a recommendation to add a pause control.

The softer wording fits the facts. The stock markup falls short, and a carousel with the missing pieces added can comply.

The TYPO3 Bootstrap Package took that route, adding role="region" and aria-roledescription="carousel" to its carousel templates.

What changed between the Bootstrap 4 and Bootstrap 5 carousel?

Bootstrap 5 prefixed every carousel data attribute with data-bs-, dropped jQuery, and rebuilt the indicators from a list into buttons. Bootstrap 5 does not read the old attribute names, so a pasted v4 snippet loses autoplay and working controls.

ItemBootstrap 4.6Bootstrap 5.3
Autoplay attributedata-ride=”carousel”data-bs-ride=”carousel”
Control attributesdata-target, data-slide, data-slide-todata-bs-target, data-bs-slide, data-bs-slide-to
Option attributesdata-interval, data-touchdata-bs-interval, data-bs-touch
Indicator markupol with li elementsdiv with buttons, aria-label, aria-current
Script call$(‘.carousel’).carousel()new bootstrap.Carousel(‘#myCarousel’)

Event names stay the same. So do cycle, pause, next, and prev, though v4 calls them through jQuery and v5 through an instance, and v4’s .carousel(number) became to(index).

Version 5.0.0 removed jQuery and util.js, so plugins run as plain JavaScript (the 4.6 docs say source builds need util.js). It also renamed the sr-only class to visually-hidden, which affects the Previous and Next text. Then 5.2.0 added an experimental data-bs-config attribute that accepts options as a JSON string.

Version 5.0.0 also swapped the chevron control icons for SVGs from Bootstrap Icons.

It dropped Internet Explorer 10 and 11 as well. If your audience still includes old browsers, test cross-browser compatibility before upgrading.

Build new carousels on 5.3, currently 5.3.8. Migrating an old one starts with renaming the attributes and rewriting the indicators.

Why is a Bootstrap carousel not working?

Beyond a missing .active slide or a mismatched id, the failures Bootstrap documents come from initialization, framework conflicts, and selectors. Check those first.

ProblemCauseFix
Swipe does nothing until a control is clickedNo data-bs-ride, so touch listeners are not registered until a control or indicator is usedCreate the instance on load with new bootstrap.Carousel(‘#id’)
Same carousel initialized twicedata-bs-ride=”carousel” plus the constructor on one elementKeep one route, since the docs say not to combine them
Carousel misbehaves inside React, Vue, or AngularBootstrap and the framework both mutate the same DOM elementUse React-Bootstrap, ng-bootstrap, or BootstrapVueNext (alpha in the 5.3 docs) instead of Bootstrap’s JavaScript
Nothing moves with JavaScript disabledThe plugins have no built-in fallbackAdd a noscript notice or your own static fallback

Ids deserve a second look too. Since 5.3.0, Bootstrap runs the id in data-bs-target and in selector strings through CSS.escape, so an id such as hero:main works there. Your own querySelector calls still need it escaped.

Start ids with a letter and skip colons and dots, and the problem never comes up.

When is the Bootstrap carousel the wrong choice?

Use it for a single-slide rotator on a page that already loads Bootstrap. Move to Swiper or Splide for multi-slide layouts, and drop rotation entirely when users must see the content.

Do users see what a carousel shows?

Nielsen Norman Group (January 2013) watched a user miss a prominent discount on Siemens’ U.K. appliance homepage because the panel auto-rotated every 5 seconds. The offer was visible 20% of the time.

Bootstrap’s default interval is the same 5000 ms. With autoplay on, five slides each get about 20% of screen time, and three slides get about 33%.

Click data from the University of Notre Dame web team (Erik Runyon, January to June 2013) points the other way. On ND.edu, 1.07% of homepage visitors clicked a feature, and position one took 89.1% of those clicks. The only autoplaying site in the set, an aggregated news site, saw 9.409% click a feature.

The two sources disagree on autoplay. NN/g’s study points against it, while Runyon’s autoplaying site drew the highest feature click rate of his five sites.

Runyon does not credit autoplay for that, and a news aggregator’s content differs from a university’s, so the data cannot settle the question. Both datasets are from 2013, and no newer first-party carousel click data was found.

When the content decides a purchase or a task, show it as static content. Keeping the carousel for items users can skip protects the user experience of everything else on the page.

Bootstrap carousel, Swiper, or Splide?

Two libraries cover what the built-in component does not. A hand-built JavaScript carousel is the other route, and the most work.

Bootstrap carousel

It ships with Bootstrap, so there is no extra download, and the basic case needs no script because data attributes do the configuring. Controls, indicators, captions, fade, and touch swipe are all included.

One slide shows at a time and nesting is out. The pause control and ARIA semantics are yours to add, and slide dimensions need manual sizing.

Swiper

Swiper shows several slides per view through slidesPerView, takes per-breakpoint configuration, has an A11y module that adds ARIA labels and roles, and comes with official React and Vue components. The price is a second library next to Bootstrap, with its own modules and stylesheet to import.

Splide

Splide’s README (v4.1.4) lists 29 kB, 12 kB gzipped, and no dependencies.

ProsCons
Multiple slides, thumbnail slider, and nested slidersOwn markup (splide__track, splide__list, splide__slide) and API, so Bootstrap attributes do not carry over
Autoplay with a play-pause button, plus keyboard control and ARIA attributesA separate stylesheet and script to load

Bootstrap Carousel FAQ

How do you change the carousel arrow icons?

Override the $carousel-control-prev-icon-bg and $carousel-control-next-icon-bg Sass variables with your own SVG data URI before compiling.

On the compiled CSS, replace the background image on the two control icon spans instead.

Can a Bootstrap carousel show more than one item per slide?

Yes, through custom markup. Put a row of cards inside each carousel item and the carousel advances one group at a time.

Sliding single cards takes a library such as Swiper (slidesPerView) or Splide.

How do you add thumbnail navigation?

Style each indicator button as a small image. The buttons already jump to a slide through data-bs-slide-to, so only the look changes.

Splide ships a thumbnail slider if hand-styling is not worth it.

How do you make a full-screen carousel?

Add vh-100 and object-fit-cover to each slide image, next to w-100, so photos fill the viewport without stretching.

Keep the carousel outside any .container wrapper, or it stays at the container width.

Which package should I use for a Bootstrap carousel in React?

React-Bootstrap. Bootstrap’s docs list it as the React option and link a working Next.js demo in the twbs/examples repository.

Can you nest one carousel inside another?

No. Bootstrap’s docs state that nested carousels are not supported.

For a slider inside a slider, Splide’s feature list includes a nested slider.

What to Verify After Launching a Bootstrap Carousel

Check keyboard operation first, then reduced-motion behavior, then first-slide loading.

  • Tab through controls and indicators, then stop autoplay without a mouse
  • Toggle the operating system’s reduced motion setting
  • Confirm slide one loads eagerly and later slides load lazily

The order follows the cost of failure. A keyboard dead end blocks a user outright, a motion setting changes what a user sees, and a slow first slide only delays it.

Each fix adds code the stock component lacks, and every Bootstrap upgrade means retesting it.

Position verified against Bootstrap 5.3.8 in October 2026. Re-check the data attribute names, the deprecated .carousel-dark class, and the Sass variables when a new major version ships.

A full web accessibility checklist extends these three checks to the rest of the page.