The Bootstrap 5 carousel is one of the most frequently used components in front-end development — and one of the most frequently implemented poorly. Default slide behaviour, misaligned controls, and carousels that break on mobile are common complaints. Done right, a responsive carousel Bootstrap implementation handles fade transitions, keyboard navigation, touch swipe, and custom controls without a single third-party dependency. This guide walks through everything from basic markup to advanced customisation, including fade mode, CSS variable overrides, and accessible control patterns.
Key Takeaways
- Bootstrap 5 ships a fully JavaScript-driven carousel with no jQuery dependency.
- The
carousel-fadeclass switches slide transitions to a crossfade effect. - Custom controls and indicators can be positioned, styled, and animated entirely through CSS.
- The
data-bs-ride="carousel"attribute enables autoplay; interval is set per-slide or globally. - Touch and swipe support is built in — disable it selectively with
data-bs-touch="false". - Accessibility requires
aria-labelon controls androle="group"with a descriptive label on the root element. - Templates like the Canvas HTML Template extend Bootstrap’s carousel with pre-built hero sliders, testimonial carousels, and logo sliders.
How the Bootstrap 5 Carousel Works
Bootstrap 5’s carousel is controlled entirely through data attributes and a small JavaScript module included in bootstrap.bundle.min.js. There is no jQuery requirement. The component cycles through .carousel-item elements inside a .carousel-inner wrapper, responding to previous/next controls and optional dot indicators.
The core anatomy looks like this:
<div id="heroCarousel" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active">
<img src="slide-1.jpg" class="d-block w-100" alt="Slide one">
</div>
<div class="carousel-item">
<img src="slide-2.jpg" class="d-block w-100" alt="Slide two">
</div>
<div class="carousel-item">
<img src="slide-3.jpg" class="d-block w-100" alt="Slide three">
</div>
</div>
</div>
Key points: exactly one .carousel-item must carry the active class on page load, data-bs-ride="carousel" triggers autoplay immediately, and data-bs-ride="true" waits for the first manual interaction before cycling. The default interval between slides is 5000ms (5 seconds), overridable with data-bs-interval="3000" on any individual slide.

Adding a Fade Transition
Replacing the default horizontal slide animation with a crossfade requires one additional class on the root element: carousel-fade. Bootstrap handles the opacity keyframes internally — no custom CSS needed.
<div id="fadeCarousel" class="carousel carousel-fade slide" data-bs-ride="carousel">
<div class="carousel-inner">
<div class="carousel-item active" data-bs-interval="4000">
<img src="hero-1.jpg" class="d-block w-100" alt="First slide">
<div class="carousel-caption d-none d-md-block">
<h5>Design that converts</h5>
<p>Build faster with production-ready components.</p>
</div>
</div>
<div class="carousel-item" data-bs-interval="6000">
<img src="hero-2.jpg" class="d-block w-100" alt="Second slide">
</div>
</div>
</div>
The carousel-fade class sets the non-active items to opacity: 0 with a CSS transition on opacity. Because the items are still positioned absolutely during the transition, you should ensure your slides share the same aspect ratio or fixed height — otherwise the carousel height will collapse. A common fix is to set a minimum height on .carousel-inner or use an aspect-ratio utility.
<style>
#fadeCarousel .carousel-inner {
aspect-ratio: 16 / 6;
}
#fadeCarousel .carousel-item img {
height: 100%;
object-fit: cover;
}
</style>
Custom Controls and Indicators
Bootstrap’s default previous/next chevron controls are functional but visually generic. Replacing them is straightforward — the controls just need data-bs-target, data-bs-slide, and appropriate ARIA attributes.
<!-- Custom prev/next buttons -->
<button class="carousel-control-prev" type="button"
data-bs-target="#heroCarousel" data-bs-slide="prev"
aria-label="Previous slide">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
</button>
<button class="carousel-control-next" type="button"
data-bs-target="#heroCarousel" data-bs-slide="next"
aria-label="Next slide">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
</button>
To style controls completely from scratch — for example, using pill-shaped buttons positioned below the carousel rather than overlaid on slides:
<style>
.carousel-control-prev,
.carousel-control-next {
width: auto;
top: auto;
bottom: -3rem;
opacity: 1;
}
.carousel-control-prev { left: calc(50% - 4rem); }
.carousel-control-next { right: calc(50% - 4rem); }
.carousel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: 50%;
background-color: var(--cnvs-themecolor, #1abc9c);
border: none;
color: #fff;
font-size: 1.25rem;
cursor: pointer;
transition: opacity 0.2s;
}
.carousel-btn:hover { opacity: 0.85; }
</style>
<button class="carousel-control-prev carousel-btn" type="button"
data-bs-target="#heroCarousel" data-bs-slide="prev"
aria-label="Previous slide">←</button>
<button class="carousel-control-next carousel-btn" type="button"
data-bs-target="#heroCarousel" data-bs-slide="next"
aria-label="Next slide">→</button>
Notice the use of var(--cnvs-themecolor) — a CSS custom property exposed by Canvas that lets you reference the active brand colour across all components without hardcoding hex values. If you are working with the Canvas HTML Template, this variable is already declared at :root and propagates through every pre-built component.
For dot indicators, Bootstrap uses a .carousel-indicators list. Each <button> requires data-bs-target, data-bs-slide-to, and aria-label:
<div class="carousel-indicators">
<button type="button" data-bs-target="#heroCarousel"
data-bs-slide-to="0" class="active" aria-current="true"
aria-label="Slide 1"></button>
<button type="button" data-bs-target="#heroCarousel"
data-bs-slide-to="1" aria-label="Slide 2"></button>
<button type="button" data-bs-target="#heroCarousel"
data-bs-slide-to="2" aria-label="Slide 3"></button>
</div>

Making the Carousel Fully Responsive
A responsive carousel Bootstrap implementation accounts for three concerns: image scaling, caption readability on small screens, and touch behaviour. Bootstrap handles image scaling when you apply d-block w-100 to the <img> tag. Captions need manual intervention.
The .carousel-caption element is hidden on small screens by default via d-none d-md-block. For a more nuanced approach, adjust caption font size and padding responsively:
<style>
@media (max-width: 767.98px) {
.carousel-caption {
display: block !important;
bottom: 1rem;
padding: 0.5rem 1rem;
background: rgba(0,0,0,0.45);
border-radius: 0.375rem;
}
.carousel-caption h5 { font-size: 1rem; }
.carousel-caption p { display: none; }
}
</style>
Touch swipe is enabled by default. To disable it on a specific carousel — useful for data visualisation slides where swipe might conflict with scroll — add data-bs-touch="false":
<div id="dataCarousel" class="carousel slide"
data-bs-ride="carousel"
data-bs-touch="false">
For multi-item carousels (showing two or three cards simultaneously) Bootstrap does not provide a built-in multi-item mode. The common pattern is to use a grid inside a single .carousel-item, then duplicate items across slides manually. Alternatively, a dedicated slider library such as Splide or Swiper handles this scenario more cleanly. This is also the approach taken in the Canvas HTML Template, which bundles Swiper for testimonial and logo carousels while reserving Bootstrap’s native carousel for hero and feature sliders.
Controlling the Carousel Programmatically
Bootstrap 5 exposes a full JavaScript API on the Carousel class. You can initialise a carousel, control it, and listen for events:
<script>
// Initialise with options
const myCarousel = new bootstrap.Carousel(
document.getElementById('heroCarousel'),
{
interval: 4000,
wrap: true,
keyboard: true,
pause: 'hover'
}
);
// Programmatic control
document.getElementById('pauseBtn').addEventListener('click', () => {
myCarousel.pause();
});
document.getElementById('playBtn').addEventListener('click', () => {
myCarousel.cycle();
});
// Event listener: fires before a slide transition starts
document.getElementById('heroCarousel').addEventListener('slide.bs.carousel', event => {
console.log('Sliding to index:', event.to);
});
// Event listener: fires after transition completes
document.getElementById('heroCarousel').addEventListener('slid.bs.carousel', event => {
console.log('Now showing index:', event.to);
});
</script>
The pause: 'hover' option stops autoplay when the user hovers over the carousel and resumes on mouse-out — a small but significant usability improvement. The wrap: false option stops cycling after the last slide, which is appropriate for onboarding flows or step-by-step sequences. If you are building out complex interactive layouts, reviewing Bootstrap 5 Modal: Triggers, Sizes, and Custom Animations is worthwhile — the event model and JavaScript API follow the same patterns.
Accessibility Best Practices for Carousels
Carousels present significant accessibility challenges. Auto-advancing content can disorient users with cognitive disabilities; motion can trigger vestibular disorders; and unlabelled controls fail screen reader users entirely. Meeting WCAG 2.1 AA requires several explicit additions beyond Bootstrap’s defaults.
- Add
role="group"andaria-roledescription="carousel"to the root element. - Add
aria-label="[Descriptive name]"to the root element. - Add
role="group",aria-roledescription="slide", andaria-label="Slide N of N"to each.carousel-item. - Provide a visible pause/play mechanism for auto-advancing carousels.
- Respect the
prefers-reduced-motionmedia query by removing transitions.
<style>
@media (prefers-reduced-motion: reduce) {
.carousel-item {
transition: none !important;
}
.carousel-fade .carousel-item {
transition: none !important;
}
}
</style>
<div id="a11yCarousel"
class="carousel carousel-fade slide"
data-bs-ride="carousel"
role="group"
aria-roledescription="carousel"
aria-label="Featured projects">
<div class="carousel-inner">
<div class="carousel-item active"
role="group"
aria-roledescription="slide"
aria-label="Slide 1 of 3">
<img src="project-1.jpg" class="d-block w-100" alt="Project Alpha homepage design">
</div>
<div class="carousel-item"
role="group"
aria-roledescription="slide"
aria-label="Slide 2 of 3">
<img src="project-2.jpg" class="d-block w-100" alt="Project Beta mobile interface">
</div>
</div>
</div>
For a broader treatment of accessibility patterns across Bootstrap components, see How to Make a Bootstrap 5 Website Accessible (WCAG 2.1 AA).
Performance Considerations for Slider-Heavy Pages
Large hero carousels are a common culprit for Largest Contentful Paint (LCP) regressions. The first slide’s image is almost always the LCP element, so it must load as fast as possible. Key optimisations:
- Mark the first slide’s image with
fetchpriority="high"and avoidloading="lazy"on it. - Use
loading="lazy"on all subsequent slide images. - Serve images in WebP or AVIF format and use
<picture>with breakpoint-specific sources. - Set explicit
widthandheightattributes on images to prevent layout shifts. - Avoid background-image CSS carousels — foreground
<img>elements are preload-scannable by the browser.
<div class="carousel-item active">
<picture>
<source media="(max-width: 767px)" srcset="hero-mobile.webp" type="image/webp">
<source media="(min-width: 768px)" srcset="hero-desktop.webp" type="image/webp">
<img src="hero-desktop.jpg"
class="d-block w-100"
alt="Agency hero — design and development"
width="1440" height="600"
fetchpriority="high">
</picture>
</div>
FAQ
Remove data-bs-ride="carousel" from the root element entirely. If you want the carousel to respond to manual controls only, omit the attribute or set data-bs-ride="false". You can also pause an already-initialised carousel via the JavaScript API: bootstrap.Carousel.getInstance(el).pause().
The interval between slides is set with data-bs-interval="3000" (milliseconds) on individual .carousel-item elements or globally on the root element. The CSS transition duration itself is defined in Bootstrap’s Sass variable $carousel-transition-duration (default 600ms). You can override this in compiled CSS with .carousel-item { transition-duration: 1s; }.
The slide class enables the horizontal sliding transition between items. Adding carousel-fade alongside it replaces the horizontal motion with a crossfade (opacity transition). Both classes coexist on the root element — carousel-fade overrides the transform-based animation defined by slide using higher-specificity CSS rules.
Bootstrap 5 does not natively support showing multiple items per slide in a continuous loop. The most reliable approach is to nest a .row with multiple columns inside a single .carousel-item, then duplicate content across slides for the overlap effect. For truly continuous multi-item sliders, integrating a dedicated library such as Swiper.js or Splide.js — both used in the Canvas HTML Template — is the cleaner solution.
Apply height: 100vh to .carousel, .carousel-inner, and .carousel-item, then set object-fit: cover on the <img> elements inside each slide. Combine this with overflow: hidden on .carousel-inner and set the image dimensions to width: 100%; height: 100% to fill the viewport without distortion. Make sure to test on iOS Safari, where 100vh includes the browser chrome in some versions — 100dvh resolves this in modern browsers.
Looking for a production-ready Bootstrap 5 HTML template? Browse Canvas Template demos and find the perfect starting point for your next project.
If you’re working with the Canvas HTML Template and want to generate production-ready layouts faster, try Canvas Builder free and see how much time you save on every project.
Skip the setup — build it free
Spin up a complete Bootstrap 5 site, blog included, with Canvas Builder. No coding, no cost.
Canvas Team
Tutorials and tips for building beautiful Bootstrap 5 websites with the Canvas HTML Template and Canvas Builder.
More from the Canvas Blog