Why I Chose Swiper.js for My Webflow Carousel (And How I Made It Accessible)

Carousels are one of the most common UI patterns on the web—but they’re also one of the most commonly inaccessible. When I set out to build a dynamic, accessible carousel for Webflow, I knew I didn’t want to reinvent the wheel. I turned to Swiper.js, one of the most powerful and popular carousel libraries available today.
However, I quickly discovered a major issue: Swiper.js is not WCAG conformant out of the box. So I rolled up my sleeves, dove into the DOM, and made a version that works beautifully with keyboard navigation, screen readers, and dynamic content from the Webflow CMS.
Today, I’m sharing the exact step-by-step process to help other Webflow developers create their own accessible Swiper.js implementations.
Swiper.js is modern, lightweight, responsive, mobile-friendly, and extremely flexible. It supports:
For my use case—building a customizable, CMS-driven carousel in Webflow—Swiper was the clear winner. But the default accessibility features were not up to WCAG 2.1 standards.
While Swiper.js includes an "a11y" module, several critical accessibility barriers remain out of the box:
role="list"
with role="listitem"
on slidesSwiper adds role="list"
to the wrapper and role="listitem"
to each slide. This is inappropriate for most carousel contexts where each slide is its own visual and interactive unit. Instead, each slide should use role="group"
with aria-roledescription="slide"
to accurately describe its purpose and provide clarity to screen reader users.
By default, Swiper pagination bullets are <span>
elements with no aria-label
, making them:
Swiper provides no dynamic feedback when a slide changes. As a result:
Swiper’s default pagination bullets:
I used Swiper’s renderBullet
option to output semantic <button>
elements for pagination, each with an aria-label
to clearly indicate which slide the button navigates to.
I added a visually hidden live region (.sr-only
) after the Swiper container to announce slide changes to screen readers.
I stripped out Swiper's default role="list"
and aria-live="off"
attributes. Instead, I manually added role="group"
and aria-roledescription="slide"
to each .swiper-slide
to provide accurate semantics.
Custom JavaScript listens for Swiper events like slideChangeTransitionEnd
, and manually updates the live region with a message like “Showing slide 2.” I also added listeners for the prev/next buttons and pagination bullets.
I added CSS to:
I tested my implementation with:
I also encourage every developer to do the same.
Paste this into your Webflow Page Settings > Footer:
<script defer>
document.addEventListener("DOMContentLoaded", function () {
const swiper = new Swiper('.swiper', {
direction: 'horizontal',
loop: true,
pagination: {
el: '.swiper-pagination',
clickable: true,
renderBullet: function (index, className) {
return '<button type="button" class="' + className + '" aria-label="Go to slide ' + (index + 1) + '"></button>';
},
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
const wrapper = document.querySelector('.swiper-wrapper');
if (wrapper) {
wrapper.removeAttribute('aria-live');
wrapper.removeAttribute('role');
}
const liveRegion = document.createElement('div');
liveRegion.className = 'sr-only custom-swiper-live';
liveRegion.setAttribute('aria-live', 'polite');
liveRegion.setAttribute('aria-atomic', 'true');
document.querySelector('.swiper').insertAdjacentElement('afterend', liveRegion);
function announceMessage(message) {
liveRegion.textContent = message;
setTimeout(() => {
liveRegion.textContent = '';
}, 1000);
}
swiper.on('slideChangeTransitionEnd', function() {
announceMessage("Showing slide " + (swiper.realIndex + 1));
});
const prevButton = document.querySelector('.swiper-button-prev');
const nextButton = document.querySelector('.swiper-button-next');
if (prevButton) {
prevButton.addEventListener('click', function() {
announceMessage("Showing previous slide");
});
}
if (nextButton) {
nextButton.addEventListener('click', function() {
announceMessage("Showing next slide");
});
}
const paginationContainer = document.querySelector('.swiper-pagination');
if (paginationContainer) {
paginationContainer.addEventListener('click', function(event) {
if (event.target && event.target.tagName === 'BUTTON') {
const buttons = Array.from(paginationContainer.querySelectorAll('button'));
const index = buttons.indexOf(event.target);
announceMessage("Showing slide " + (index + 1));
}
});
}
});
</script>
.sr-only
Class!This live region needs to be visually hidden but still accessible to screen readers. Add this CSS to your Webflow project:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
border: 0;
}
.swiper {
width: 100%;
height: 100%;
position: relative;
}
.swiper-pagination {
position: relative;
bottom: 36px !important;
}
.swiper-pagination-bullet {
width: 24px;
height: 24px;
opacity: 0.5;
}
.swiper-pagination-bullet-active {
background-color: #000;
opacity: 0.9;
}
.swiper-pagination-bullet:focus-visible {
outline: -webkit-focus-ring-color auto 1px;
outline-offset: 3px;
}
.swiper-pagination-horizontal {
width: auto !important;
top: 10px !important;
}
.swiper-button-prev,
.swiper-button-next {
color: #121212;
}
.carousel-wrapper {
position: relative;
max-width: 48rem;
margin: 0 auto;
}
.carousel-wrapper .swiper {
width: 100%;
position: relative;
}
.carousel-wrapper .swiper-button-prev,
.carousel-wrapper .swiper-button-next {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
cursor: pointer;
}
.carousel-wrapper .swiper-button-prev {
left: -3rem;
}
.carousel-wrapper .swiper-button-next {
right: -3rem;
}
@media (max-width: 991px) {
.carousel-wrapper .swiper-button-prev,
.carousel-wrapper .swiper-button-next {
display: none;
}
}
You can grab the latest Swiper CSS and JS CDN links from Swiper’s official site.
Head over to my studio’s homepage to see the accessible carousel live in Webflow: 👉 https://www.gracefulwebstudio.com
If you're serious about accessibility, test your implementation using:
Swiper.js is a fantastic tool for building responsive carousels, but it requires thoughtful customization to meet accessibility standards. By following these steps, you’ll be well on your way to delivering beautiful and inclusive components in Webflow.
If you found this helpful, share it with another dev—or let me know how your own implementation went!
Happy Coding!
Send a message or request a project quote for an estimate within 24 hours. Prefer to chat? Book a call, and let’s find the right solution for you!