
Using the Bootstrap Carousel in Views I found an issue with break points. You can set the number of "slides" or images shown at one time 1-4 images. Unfortunately, it remains at the set number through breakpoints or screen sizes causing the carousel to stack. Which doesn't look very pleasant and can break user experience.
I fixed this issue with a twig for the view.
Assumptions
- You are using the Bootstrap Carousel markup.
- You want the number of visible items per slide to change at different breakpoints (using Bootstrap grid classes).
- The view provides a variable called rows, each being a rendered item (category).
- This is for a block display.
What will be fixed in this tutorial
- Always enables swipe/touch/controls (even for one group of items)
- Never breaks or gets stuck when sliding between duplicated slides
- Handles dynamic resizing and column changes
- Works across all devices and browsers
- Has fully responsive font sizes for captions and text
Whether you’re building a product showcase, a testimonials slider, or any multi-item carousel, this guide is for you!
The Bootstrap Carousel Problem
Bootstrap’s Carousel is fantastic—except when you have just one or two slides. In these cases, you’ll notice:
- Swipe and controls are sometimes disabled.
- The carousel can break, get stuck, or stop responding after resizing or interacting with controls.
- Dynamic carousels with changing column counts (responsive layouts) can be especially fragile.
Why does this happen?
Bootstrap’s internal logic expects at least three distinct .carousel-item
elements for seamless, infinite sliding and wrapping. With only one or two, it can become confused, especially if you duplicate slides to fake more.
The Solution: Always Render at Least Three Slides
The key insight:
If you have only one or two logical slides, duplicate them so that there are at least three carousel items in the DOM.
- If you have one slide: render it three times.
- If you have two slides: render as A, B, A.
- For three or more slides, render as usual.
This tricks Bootstrap into keeping all its touch/swipe logic enabled and prevents it from breaking at the edges!
Responsive Multi-Column Carousel JavaScript
Here’s a robust, fully-commented implementation. It supports:
- Dynamic grouping for multi-column slides at any breakpoint
- Fully responsive controls and indicators
- Automatic duplication for one/two slides
- Touch/swipe and controls always enabled
- Works with Bootstrap 5+
/**
* Responsive Bootstrap Multi-Column Carousel (Robust, Always Slides, Touch Enabled, Never Breaks)
* - 100% width, responsive, dynamic regrouping, multi-carousel support.
* - Cleans col-* classes only once per element.
* - Fills last slide with clones for infinite wrap.
* - Guarantees at least 3 slides for robust Bootstrap wrapping (even with 1 or 2 logical slides).
* - Always enables swipe/touch/controls (even for one slide by duplicating).
* - Resets indicators (if present) to match slide count.
* - Debounced resize/orientation handler.
* - Robustly pauses, disposes, and resets Bootstrap carousel before and after DOM rebuild.
*/
(function () {
const breakpoints = [
{ min: 992, columns: 4 },
{ min: 768, columns: 3 },
{ min: 0, columns: 1 }
];
function getColumnsPerSlide() {
const width = window.innerWidth;
for (const bp of breakpoints) {
if (width >= bp.min) return bp.columns;
}
return 1;
}
function getColClass(numCols) {
const size = Math.floor(12 / numCols);
return 'col-' + Math.max(1, Math.min(size, 12));
}
function empty(el) {
while (el.firstChild) el.removeChild(el.firstChild);
}
function chunkArray(arr, chunkSize) {
const out = [];
for (let i = 0; i < arr.length; i += chunkSize) {
out.push(arr.slice(i, i + chunkSize));
}
return out;
}
function cleanColClasses(element) {
const clone = element.cloneNode(true);
clone.className = clone.className.replace(/col-\w+(\d+)?/g, '').replace(/\s+/g, ' ').trim();
return clone;
}
function getAllCols(carouselInner) {
const cols = [];
const rows = carouselInner.querySelectorAll('.carousel-item .row');
for (const row of rows) {
for (const col of row.children) {
if (/col-/.test(col.className)) {
cols.push(cleanColClasses(col));
}
}
}
return cols;
}
// Rebuild indicators to match slides
function rebuildIndicators(carouselElem, numSlides) {
const indicators = carouselElem.querySelector('.carousel-indicators');
if (!indicators) return;
empty(indicators);
for (let i = 0; i < numSlides; i++) {
const li = document.createElement('button');
li.type = "button";
li.setAttribute('data-bs-target', '#' + carouselElem.id);
li.setAttribute('data-bs-slide-to', i);
if (i === 0) li.className = 'active';
li.setAttribute('aria-current', i === 0 ? 'true' : 'false');
li.setAttribute('aria-label', 'Slide ' + (i + 1));
indicators.appendChild(li);
}
}
function robustReinitBootstrapCarousel(carouselElem) {
if (typeof bootstrap !== "undefined" && bootstrap.Carousel) {
let instance = bootstrap.Carousel.getInstance(carouselElem);
if (instance) {
instance.pause();
instance.dispose();
}
// Remove .active from all, set first as active
const items = carouselElem.querySelectorAll('.carousel-item');
items.forEach((item, idx) => {
item.classList.remove('active');
if (idx === 0) item.classList.add('active');
});
// Always re-init (we guarantee there are at least 3 slides)
new bootstrap.Carousel(carouselElem, {
interval: false,
ride: false,
pause: true,
wrap: true,
touch: true,
keyboard: true
});
}
}
function rebuildAllCarousels() {
const carouselInners = document.querySelectorAll('.carousel-inner');
for (const carouselInner of carouselInners) {
const carouselElem = carouselInner.closest('.carousel');
// Always pause/dispose before DOM changes
if (carouselElem && typeof bootstrap !== "undefined" && bootstrap.Carousel) {
let instance = bootstrap.Carousel.getInstance(carouselElem);
if (instance) {
instance.pause();
instance.dispose();
}
}
const allCols = getAllCols(carouselInner);
if (allCols.length === 0) continue;
empty(carouselInner);
const columnsPerSlide = getColumnsPerSlide();
const colClass = getColClass(columnsPerSlide);
let slides = chunkArray(allCols, columnsPerSlide);
// Fill last slide with clones if needed
if (slides.length > 1 && slides[slides.length - 1].length < columnsPerSlide) {
const missing = columnsPerSlide - slides[slides.length - 1].length;
for (let i = 0; i < missing; i++) {
slides[slides.length - 1].push(allCols[i % allCols.length].cloneNode(true));
}
}
// --- Robust touch/swipe/wrap fix for single/two slides ---
// If only one slide, duplicate it twice (three identical slides)
if (slides.length === 1) {
for (let i = 0; i < 2; ++i) {
slides.push(slides[0].map(col => col.cloneNode(true)));
}
}
// If only two slides, duplicate the first to make three (A, B, A)
else if (slides.length === 2) {
slides.push(slides[0].map(col => col.cloneNode(true)));
}
// Build carousel items
for (let i = 0; i < slides.length; i++) {
const itemDiv = document.createElement('div');
itemDiv.className = 'carousel-item' + (i === 0 ? ' active' : '');
const rowDiv = document.createElement('div');
rowDiv.className = 'row g-0';
for (const col of slides[i]) {
col.classList.add(colClass);
rowDiv.appendChild(col);
}
itemDiv.appendChild(rowDiv);
carouselInner.appendChild(itemDiv);
}
// Always show controls and indicators (no hiding)
if (carouselElem) {
// Rebuild indicators if present
const numSlides = slides.length;
rebuildIndicators(carouselElem, numSlides);
['.carousel-control-prev', '.carousel-control-next', '.carousel-indicators'].forEach(selector => {
carouselElem.querySelectorAll(selector).forEach(el => {
el.style.display = '';
});
});
robustReinitBootstrapCarousel(carouselElem);
}
}
}
let debounceTimeout;
function debounce(fn, delay) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(fn, delay);
}
function onResize() {
debounce(rebuildAllCarousels, 100);
}
document.addEventListener('DOMContentLoaded', function () {
rebuildAllCarousels();
window.addEventListener('resize', onResize, { passive: true });
window.addEventListener('orientationchange', onResize);
});
window.updateCarousel = rebuildAllCarousels;
})();
Responsive, Accessible, and Modern Carousel CSS
Don’t forget the CSS! Here’s a modern, accessible, and responsive style set, including fluid font sizes for captions:
/* 1. Add outer padding to the carousel container */
.carousel {
padding: 3px;
font-size: clamp(0.95rem, 1.5vw, 1.13rem); /* Responsive base font */
}
/* 2. Ensure all images are the same size and touch each other */
.carousel .carousel-item img {
width: 100%;
height: 245px;
object-fit: cover;
display: block;
padding: 0;
margin: 0;
}
.carousel-inner img {
opacity: 0.7;
transition: opacity 0.3s ease-in-out;
}
.carousel-inner img:hover {
opacity: 1; /* Fully visible on hover */
}
/* Remove spacing between columns */
.carousel .row {
margin: 0;
}
.carousel .row > div[class*="col-"] {
padding: 0;
}
/* 3. Center and style the title/term name */
.carousel-caption {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
bottom: 30%;
}
/* Responsive font for .carousel-caption h3 */
.carousel-caption h3 {
font-size: clamp(1.05rem, 2vw, 1.5rem);
color: white;
line-height: 1.2;
margin: 0.5em 0 0 0;
font-weight: 600;
}
@media (max-width: 575.98px) {
.carousel-caption h3 {
font-size: 1rem;
}
}
@media (min-width: 1400px) {
.carousel-caption h3 {
font-size: 1.8rem;
}
}
/* 4. Style the icon inside the image at the bottom right */
.carousel .position-relative {
position: relative;
}
.carousel .term-icon {
position: absolute;
bottom: 5px;
right: 5px;
z-index: 2;
width: 32px;
height: 32px;
}
.carousel .term-icon img {
width: 100%;
height: 100%;
object-fit: contain;
}
5. How to Use: Step-By-Step
- Include Bootstrap CSS/JS (v5+ recommended).
- Add your carousel HTML (see Bootstrap docs).
- Add the JavaScript and CSS from above.
- Make sure each carousel
.carousel-inner
contains at least one.carousel-item
with a.row
of your column content. - Enjoy a carousel that never breaks, always slides, and looks great on any device!
SEO, Accessibility, and Performance Tips
- Ensure all images have meaningful
alt
attributes. - Use semantic HTML for captions and text.
- Use lazy-loading for images if you have many slides.
- Add
aria-label
and roles to the carousel if needed for screen readers.
Conclusion
With this robust solution, you’ll never worry about your Bootstrap carousel breaking with dynamic, single, or double slides again.
Your users get a seamless, mobile-friendly experience—and you get peace of mind.
Have questions or want to share your results? Leave a comment below!