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

Share this post

Screenshot of the Swiper.js homepage showing its tagline “The Most Modern Mobile Touch Slider” on the left and a carousel of media thumbnails on the right, with a large distressed-style stamp overlay reading “#A11Y” to emphasize accessibility.

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.

Swiper The Most Modern Mobile Touch Slider with Endless Creatiity

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.

Why Swiper.js?

Swiper.js is modern, lightweight, responsive, mobile-friendly, and extremely flexible. It supports:

  • Touch interactions
  • Navigation buttons and pagination
  • Looping
  • Dynamic slides
  • Accessibility module (which needs improvement, but it’s a good start)

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.

Accessibility Problems with Swiper.js by Default

While Swiper.js includes an "a11y" module, several critical accessibility barriers remain out of the box:

🚫 role="list" with role="listitem" on slides

Swiper 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.

🚫 Pagination bullets are not real buttons

By default, Swiper pagination bullets are <span> elements with no aria-label, making them:

🚫 No live announcements of slide changes

Swiper provides no dynamic feedback when a slide changes. As a result:

  • Screen reader users have no indication that the visible slide has changed
  • This fails WCAG 4.1.2, which requires that important changes are communicated via accessible name/role/value updates or live regions

🚫 Touch target and contrast issues

Swiper’s default pagination bullets:

How I Made It Accessible

1. Use Custom Pagination Buttons

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.

2. Create a Live Region for Announcements

I added a visually hidden live region (.sr-only) after the Swiper container to announce slide changes to screen readers.

3. Remove Inappropriate ARIA Roles

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.

4. Announce Slide Changes Dynamically

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.

5. Improve Touch Targets and Visual Focus

I added CSS to:

  • Enforce a minimum 24x24px size on pagination bullets
  • Provide strong visual contrast between active and inactive states
  • Add a clear focus-visible outline for keyboard users

6. Encourage Comprehensive Testing

I tested my implementation with:

  • Keyboard-only navigation
  • Mouse-only interaction
  • Touch screens
  • Screen readers like VoiceOver and NVDA

I also encourage every developer to do the same.

Full Code Snippet

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>

Don’t Forget the .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;
}

Additional CSS for Pagination and Navigation Accessibility

.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;
  }
}

Where to Get Swiper.js CDN

You can grab the latest Swiper CSS and JS CDN links from Swiper’s official site.

Want to See It in Action?

Head over to my studio’s homepage to see the accessible carousel live in Webflow: 👉 https://www.gracefulwebstudio.com

My Accessible Carousel

Test It Like a Pro

If you're serious about accessibility, test your implementation using:

Conclusion

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!

Your Website Deserves Better, Let's Team Up

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!