Accessible Swiper JS Carousel (Autoplay + A11y)


This article is Part Three in our series on building WCAG-conformant SwiperJS carousels in Webflow. If you missed the first two, check them out here:
Today we’re diving into autoplay, one of the trickiest parts of building an accessible carousel. I'll walk you through how to implement an autoplaying testimonial slider that is fully WCAG-conformant using SwiperJS, complete with:
Let’s build it step-by-step.
1.1. Add a Collection list from Add panel > Elements > CMS
Give the Collection list wrapper the class: swiper & swiper-testimonials
Add custom attributes:
role="region"aria-roledescription="carousel"aria-labelledby="carousel-testimonial-heading" (targeting the labeling heading)1.2. On the nested Collection list
Set the class to swiper-wrapper
1.2. On the nested Collection item
Set the class to swiper-slide (Swiper will automatically add the required role="group" and an aria-label="# / #")
1.4. Inside each CMS item, add your testimonial content with a rich text element.
In your CMS add the quote in a rich text element so you can use the blockquote tag.
For demo purposes, use quotes like:
“The service was excellent, and I’d recommend them to anyone.” — Jane, CA
1.5. Swiper.js automatically adds a span element with the class: swiper-notification with the attributes aria-live="assertive" aria-atomic="true"
<span class="swiper-notification" aria-live="assertive" aria-atomic="true"></span>
1.6. Add a pagination bullets container:
Create a div and assign it the class: swiper-pagination & swiper-pagination-testimonials
Add custom attributes:
role="group"aria-label="Slide Controls"Swiper.js will add the pagination buttons for you with the correct attributes like this:
<span class="swiper-pagination-bullet swiper-pagination-bullet-active" tabindex="0" role="button" aria-label="Go to slide 1" aria-current="true"></span>
To meet WCAG 2.2 requirements for pause, stop, and hide, we must give users an accessible way to pause the autoplay. These buttons are not optional when using Swiper’s autoplay functionality. This solution uses accessible, ARIA-compliant controls that can be operated with mouse, keyboard, or assistive technology.
Each button is:
role="group" for semantic clarityaria-label such as "Pause Testimonials" or "Play Testimonials" (In case you have multiple pause and play buttons on your page)aria-pressed attribute so screen reader users understand the current state#777), and the active button is black (#181818), providing a clear 3:1 color contrast ratio to meet minimum accessibility guidelines for non-text UI components💡 Pro Tip: The visual toggle and aria-pressed state must always match. We bind both state and styling in the JavaScript for seamless control.
You will need to use a code embed for the play/pause controls:
<!-- Autoplay Controls -->
<div role="group" aria-label="Testimonials Autoplay Controls" class="autoplay-controls">
<button id="pause-button-testimonials" aria-pressed="false" aria-label="Pause Testimonials" class="pause-button-carousel">
<!-- Pause Icon -->
<svg class="pause-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" fill="currentColor" />
</svg>
</button>
<button id="play-button-testimonials" aria-pressed="true" aria-label="Play Testimonials" class="play-button-carousel">
<!-- Play Icon -->
<svg class="play-icon" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M8 5v14l11-7z" fill="currentColor" />
</svg>
</button>
</div>Add this to your before </body> section or via a Webflow embed:
This JavaScript does three important things:
aria-pressed attribute. At the same time, our CSS ensures that the active button turns dark (#181818) and the inactive one light gray (#777777), achieving at least a 3:1 color contrast ratio. This satisfies both visual and screen reader feedback requirements..swiper-notification region. It clears the announcement after 1 second to avoid repetition and clutter.💡 Pro Tip: Want to confirm your announcements are working as intended? Test with a screen reader:
Screen readers will announce the testimonial change when pagination bullets are activated. This is how you verify your implementation is usable for everyone. Swiper sometimes inserts .swiper-notification outside the expected hierarchy—use fallback logic to find it safely.
If your bullets aren't announcing properly, inspect your DOM. Swiper sometimes inserts .swiper-notification outside the expected hierarchy—use fallback logic to find it safely.
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const swiperTestimonialsEl = document.querySelector(".swiper.swiper-testimonials");
if (!swiperTestimonialsEl) return;
const swiperTestimonials = new Swiper(swiperTestimonialsEl, {
autoplay: {
delay: 8000,
disableOnInteraction: false,
},
pagination: {
el: ".swiper-pagination-testimonials",
clickable: true,
},
keyboard: {
enabled: true,
onlyInViewport: true,
},
});
const playButton = document.getElementById("play-button-testimonials");
const pauseButton = document.getElementById("pause-button-testimonials");
if (playButton && pauseButton) {
playButton.addEventListener("click", () => {
swiperTestimonials.autoplay.start();
playButton.setAttribute("aria-pressed", "true");
pauseButton.setAttribute("aria-pressed", "false");
});
pauseButton.addEventListener("click", () => {
swiperTestimonials.autoplay.stop();
playButton.setAttribute("aria-pressed", "false");
pauseButton.setAttribute("aria-pressed", "true");
});
}
const liveRegion = swiperTestimonialsEl.querySelector(".swiper-notification")
|| document.querySelector(".swiper-testimonials ~ .swiper-notification")
|| document.querySelector(".swiper-notification");
const announce = (msg) => {
if (liveRegion) {
liveRegion.textContent = msg;
setTimeout(() => {
liveRegion.textContent = "";
}, 1000);
}
};
document.querySelectorAll(".swiper-pagination-testimonials .swiper-pagination-bullet").forEach((btn) => {
btn.addEventListener("click", () => announce("Showing selected testimonial"));
btn.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
announce("Showing selected testimonial");
}
});
});
});
</script>This CSS is doing a few key things:
.swiper-slide for clean layout and readability.#000) vs. inactive (#777). We also include focus-visible styling to support keyboard users..controls-wrapper and .autoplay-controls classes ensure that the pause/play buttons are stacked nicely under the bullets and stay visually accessible.aria-pressed="true". This styling works hand-in-hand with the JavaScript logic to ensure the carousel stays visually clear and compliant.<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css" />
<style>
.swiper {
max-width: 100%;
margin: auto;
}
.swiper.swiper-testimonials {
padding-bottom: 3rem;
}
.swiper-testimonials .swiper-slide {
padding: 1rem;
text-align: center;
}
.swiper-pagination-testimonials {
position: relative;
bottom: 14px !important;
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.75rem;
}
.swiper-pagination-testimonials .swiper-pagination-bullet {
width: 24px;
height: 24px;
background-color: #777;
opacity: 1;
border-radius: 50%;
transition: all 0.2s ease;
}
.swiper-pagination-testimonials .swiper-pagination-bullet-active {
background-color: #000;
opacity: 0.9;
}
.swiper-pagination-testimonials .swiper-pagination-bullet:focus-visible {
outline: -webkit-focus-ring-color auto 1px;
outline-offset: 3px;
}
.controls-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 0.5rem;
}
.autoplay-controls {
margin-top: 0.5rem;
z-index: 10;
}
.pause-icon,
.play-icon {
width: 32px;
height: 32px;
fill: #181818;
transition: fill 0.2s ease-in-out;
}
.play-button-carousel,
.pause-button-carousel {
color: #777777;
background-color: #F7F7F7;
transition: color 0.2s ease-in-out;
border: none;
padding: 0.25rem;
border-radius: 4px;
}
.play-button-carousel[aria-pressed="true"],
.pause-button-carousel[aria-pressed="true"] {
color: #181818;
}
</style>This solution brings together autoplay, accessibility, and keyboard control in one beautiful Webflow-ready component—but more importantly, it prioritizes people.
Autoplaying content can be overwhelming and even harmful to users with ADHD, motion sensitivity, or vestibular disorders. WCAG requires pause/stop functionality for moving content, and our implementation gives users full control—without sacrificing design.
The combination of aria-pressed, keyboard operability, and clear visual states (with at least 3:1 contrast between active and inactive buttons) ensures that both screen reader users and sighted keyboard users know exactly what to expect.
💬 If you need help implementing this carousel, or want a fully custom, accessible SwiperJS component for your organization or client site, I'm available for development services on a monthly retainer:👉 View Monthly Retainer Packages
Let’s make every carousel accessible—one component at a time. 🌼
We build accessible, conversion-focused Webflow websites for businesses across Yakima and the US. Crystal Scott is a Certified Professional in Web Accessibility (CPWA) with 11+ years of front-end experience.
Our services:
Transparent pricing on every service page. Request a quote and get a response within one business day.
We would love to meet with you face-to-face. Whether virtually or for a coffee. Book a call, and let’s find the right solution for you! We review your site, map goals, and then deliver a clear plan and quote.
