Building a Custom JavaScript Slider with Accessibility Features

Creating a custom JavaScript slider is an excellent way to add dynamic content presentation to your website while maintaining full control over design and behavior. However, a slider that is not accessible can exclude users with disabilities, leading to a poor user experience and potential legal compliance issues. This guide walks you through building a fully functional, accessible JavaScript slider step by step, covering HTML semantics, CSS styling, ARIA attributes, keyboard navigation, and robust JavaScript event handling.

Why Accessibility Matters for Sliders

Sliders or carousels are common UI components used to display multiple pieces of content—such as images, testimonials, or product highlights—in a rotating manner. Without proper accessibility, these components can be unusable for people who rely on screen readers, keyboard-only navigation, or switch devices. Accessible sliders ensure that all users, regardless of ability, can perceive, operate, and understand the content. Moreover, many countries have laws requiring digital accessibility, and adhering to standards like the Web Content Accessibility Guidelines (WCAG) 2.1 is a best practice for any production website.

For a deeper understanding of accessibility principles, refer to the W3C’s Web Accessibility Initiative (WAI). Additional guidance on accessible carousels is available from the WAI-ARIA Authoring Practices (we will link to these resources later).

Core Accessibility Requirements for a Slider

Before writing any code, it is important to establish the accessibility properties any slider component must support. These include:

  • Keyboard navigation: Users must be able to move between slides using arrow keys, activate controls with Enter or Space, and leave the slider with Tab.
  • ARIA roles and properties: Assigning roles like region, tablist, or listbox (depending on semantics) and providing labels via aria-label or aria-labelledby.
  • Focus management: When slides change via keyboard, focus must remain inside the slider or move to the newly active content.
  • Announcements for screen readers: Using live regions (aria-live) or aria-atomic to notify users of slide changes.
  • Pause/Hide on interaction: Autoplay must be pausable; users can stop movement that could cause confusion.

Structuring the HTML for Maximum Semantics

The foundation of an accessible slider is a clean, semantic HTML structure. Use native HTML elements wherever possible and add ARIA only to supplement when semantics are insufficient. Below is an expanded example structure that includes additional slide indicators and a pause button for autoplay.

<div class="slider" role="region" aria-label="Featured testimonials" aria-roledescription="carousel">
  <div class="slides-wrapper" aria-live="polite" aria-atomic="true">
    <div class="slide" role="group" aria-roledescription="slide" aria-label="1 of 5">
      <!-- slide content -->
    </div>
    <div class="slide" role="group" aria-roledescription="slide" aria-label="2 of 5">
      <!-- slide content -->
    </div>
    <!-- more slides -->
  </div>

  <div class="controls">
    <button class="prev" aria-label="Previous slide">‹ Prev</button>
    <button class="next" aria-label="Next slide">Next ›</button>
  </div>

  <div class="indicators" role="tablist" aria-label="Slide indicators">
    <button role="tab" aria-selected="true" aria-label="Slide 1">1</button>
    <button role="tab" aria-selected="false" aria-label="Slide 2">2</button>
    <!-- more indicator buttons -->
  </div>

  <button class="play-pause" aria-label="Pause autoplay">⏸ Pause</button>
</div>

Key points in this markup:

  • role="region" identifies the slider as a landmark for navigation.
  • aria-roledescription="carousel" helps screen readers announce the widget type exactly.
  • Each slide has role="group" and aria-roledescription="slide", plus a dynamic label like “1 of 5” that updates as slides change.
  • The aria-live="polite" on the wrapper ensures screen reader users hear announcements of slide changes without interrupting ongoing speech.
  • Indicators use role="tablist" and role="tab" to express the relationship between indicator buttons and slides. The selected indicator gets aria-selected="true".

For further reading on carousel patterns, see the WAI-ARIA Authoring Practices Guide on Carousel (we will link to this later).

Styling with CSS: Interaction and Focus Visibility

CSS must support the accessibility features programmatically. Ensure:

  • Visible focus indicators for all interactive elements (buttons, indicator buttons). Avoid removing outlines entirely; instead, style them with a custom focus ring.
  • Hidden slides are correctly removed from the accessibility tree using display: none or the hidden attribute. Do not rely solely on opacity or visibility because screen readers can still navigate to invisible content.
  • Responsive design for different viewport sizes—ensure controls are touch-friendly with adequate tap targets (at least 44x44 pixels).
  • Reduced motion support: use a prefers-reduced-motion media query to disable or simplify slide transitions for users who prefer no movement.
/* Example focus style */
.slider button:focus-visible {
  outline: 3px solid #4A90D9;
  outline-offset: 2px;
}

/* Reduced motion query */
@media (prefers-reduced-motion: reduce) {
  .slider .slide {
    transition: none !important;
  }
  .slider .slides-wrapper {
    scroll-behavior: auto !important;
  }
}

Implementing the JavaScript: Event Handlers and State Management

JavaScript ties everything together. The script must manage slide transitions, keyboard events, focus movement, ARIA attribute updates, and autoplay with pause functionality. Below is a robust implementation that follows best practices.

Selecting DOM Elements and Initializing State

We’ll query all essential elements and set initial ARIA states:

const sliderContainer = document.querySelector('.slider');
const slidesWrapper = document.querySelector('.slides-wrapper');
const slides = document.querySelectorAll('.slide');
const prevBtn = document.querySelector('.prev');
const nextBtn = document.querySelector('.next');
const indicators = document.querySelectorAll('[role="tab"]');
const playPauseBtn = document.querySelector('.play-pause');
let currentSlide = 0;
let autoplayTimer = null;
const autoplayInterval = 5000; // 5 seconds

Updating Slide Visibility and ARIA Attributes

The core function that displays the correct slide and updates all associated attributes:

function showSlide(index) {
  // Update slides
  slides.forEach((slide, i) => {
    const isActive = i === index;
    slide.classList.toggle('active', isActive);
    slide.setAttribute('aria-hidden', !isActive);
    // Update slide label: "2 of 5"
    slide.setAttribute('aria-label', `${i+1} of ${slides.length}`);
    // Remove tabindex from non-visible slides to prevent keyboard focus
    if (isActive) {
      slide.removeAttribute('tabindex');
    } else {
      slide.setAttribute('tabindex', '-1');
    }
  });

  // Update indicators
  indicators.forEach((indicator, i) => {
    const isSelected = i === index;
    indicator.setAttribute('aria-selected', isSelected);
    indicator.classList.toggle('active', isSelected);
    indicator.setAttribute('tabindex', isSelected ? '0' : '-1');
  });

  // Update container label or live region as needed
  // Optionally shift focus to the active slide or an element inside it
}

Add event listeners to the buttons:

function goToSlide(newIndex) {
  if (newIndex < 0) newIndex = slides.length - 1;
  if (newIndex >= slides.length) newIndex = 0;
  currentSlide = newIndex;
  showSlide(currentSlide);
  // After programmatic change, move focus to the newly active slide
  slides[currentSlide].focus({ preventScroll: true });
}

prevBtn.addEventListener('click', () => goToSlide(currentSlide - 1));
nextBtn.addEventListener('click', () => goToSlide(currentSlide + 1));

// Indicator click
indicators.forEach((indicator, i) => {
  indicator.addEventListener('click', () => goToSlide(i));
});

Keyboard Navigation Within the Slider

Add a keydown listener on the slider container that handles arrow keys and other shortcuts. Ensure the keys work only when focus is inside the slider.

sliderContainer.addEventListener('keydown', (e) => {
  // Check if focus is within the slider
  if (!sliderContainer.contains(document.activeElement)) return;

  switch (e.key) {
    case 'ArrowLeft':
      e.preventDefault();
      goToSlide(currentSlide - 1);
      break;
    case 'ArrowRight':
      e.preventDefault();
      goToSlide(currentSlide + 1);
      break;
    case 'Home':
      e.preventDefault();
      goToSlide(0);
      break;
    case 'End':
      e.preventDefault();
      goToSlide(slides.length - 1);
      break;
    default:
      break;
  }
});

Important: Do not interfere with the browser’s default behavior for Tab keys. Users must be able to tab into and out of the slider. The tabindex on slides is set to -1 (non-visible) to avoid trapping focus, but the active slide gets tabindex="0" to allow focus if needed.

Autoplay with Pause on Hover or Focus

Autoplay should automatically cycle slides, but it must stop when the user interacts (hover, focus on any control, or presses the pause button).

function startAutoplay() {
  if (autoplayTimer) return;
  autoplayTimer = setInterval(() => goToSlide(currentSlide + 1), autoplayInterval);
}

function stopAutoplay() {
  clearInterval(autoplayTimer);
  autoplayTimer = null;
}

// Pause on hover (mouse enter/leave) and focus
sliderContainer.addEventListener('mouseenter', stopAutoplay);
sliderContainer.addEventListener('mouseleave', startAutoplay);
sliderContainer.addEventListener('focusin', stopAutoplay);
sliderContainer.addEventListener('focusout', (e) => {
  // Restart only if focus leaves the slider entirely
  if (!sliderContainer.contains(e.relatedTarget)) {
    startAutoplay();
  }
});

// Play/Pause button toggle
playPauseBtn.addEventListener('click', () => {
  const isPaused = playPauseBtn.getAttribute('aria-pressed') === 'true';
  if (isPaused) {
    startAutoplay();
    playPauseBtn.setAttribute('aria-pressed', 'false');
    playPauseBtn.innerHTML = '⏸ Pause';
    playPauseBtn.setAttribute('aria-label', 'Pause autoplay');
  } else {
    stopAutoplay();
    playPauseBtn.setAttribute('aria-pressed', 'true');
    playPauseBtn.innerHTML = '▶ Play';
    playPauseBtn.setAttribute('aria-label', 'Resume autoplay');
  }
});

// Initialize autoplay
startAutoplay();

Note the use of aria-pressed to indicate toggle state to assistive technology.

Testing and Validation

After implementing, rigorous testing is essential. Use both automated tools and manual keyboard/navigation tests.

  • Keyboard only: Navigate through the slider using Tab, Shift+Tab, arrow keys, Enter/Space on buttons. Ensure no focus gets trapped.
  • Screen reader: Test with NVDA (Windows) or VoiceOver (macOS). Verify that slide changes are announced, controls are correctly labeled, and the slider is identified as a carousel.
  • Automated checks: Run Lighthouse (built into Chrome DevTools) or the WAVE evaluation tool. These can catch missing ARIA attributes, insufficient color contrast, and missing focus evidence.
  • Reduced motion: Enable prefers-reduced-motion: reduce in browser DevTools to verify that transitions are disabled.

For comprehensive testing guidelines, refer to the WAI’s website on Evaluating Web Accessibility.

Common Pitfalls and How to Avoid Them

  • Hiding content improperly: Avoid using display:none on all slides except one, as this can cause layout recalculation. Instead, use visibility: hidden combined with position: absolute and height:0 for non-visible slides, but always include aria-hidden="true".
  • Losing focus when sliding: After programmatically changing slides, ensure focus moves to the new slide or an appropriate control. The focus() call in goToSlide is crucial.
  • Overusing ARIA: Do not add ARIA roles or attributes that are already implicit in native HTML elements. For example, a <button> already has role="button"—do not duplicate.
  • Autoplay without pause: This is a violation of WCAG Success Criterion 2.2.2 (Pause, Stop, Hide). Always provide a mechanism to pause, stop, or hide moving content.

Expanding the Slider Beyond Basic Functionality

For production sites, consider adding:

  • Touch/swipe support: Listen for touchstart and touchend events to detect swipe gestures. Ensure gesture controls are supplementing, not replacing, keyboard and button controls.
  • Cross-fade or CSS transitions: Use CSS transitions for smooth animations, but again respect prefers-reduced-motion.
  • Dynamic content: If slides are loaded dynamically via JavaScript (e.g., from an API), update ARIA labels and indicator counts accordingly.
  • Multiple sliders on a page: Ensure each slider has a unique aria-label and that JavaScript selects the correct elements within each instance using classes or data attributes.
  • Lazy loading images: Use loading="lazy" on <img> tags inside slides to improve performance, but ensure that off-screen slides (which may not be loaded until visible) are announced correctly by screen readers—consider using aria-label with descriptive text.

Integrating with Frameworks

The vanilla JavaScript approach above can be adapted to frameworks like React, Vue, or Angular. The principles remain the same: manage state, update ARIA attributes reactively, and handle events with accessibility in mind. Many slider libraries exist, but building custom one ensures you understand every accessibility detail and can tailor it to your specific design system.

Resources and Further Reading

To deepen your understanding of accessible sliders and ARIA patterns, consult the following authoritative sources:

Conclusion

Building a custom JavaScript slider with full accessibility features requires careful planning across HTML, CSS, and JavaScript. By adhering to WAI-ARIA patterns, managing focus well, providing keyboard navigation, and respecting user preferences for reduced motion, you create a component that is usable by everyone. This not only improves the overall user experience but also demonstrates a commitment to inclusive design. The code and patterns presented in this guide serve as a solid foundation that you can adapt and extend for any project. Remember to test thoroughly with real assistive technologies to catch subtle issues that automated tools might miss—your users will thank you.