statics-and-dynamics
Building a Custom Javascript Slider with Accessibility Features
Table of Contents
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, orlistbox(depending on semantics) and providing labels viaaria-labeloraria-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) oraria-atomicto 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: noneor thehiddenattribute. Do not rely solely onopacityorvisibilitybecause 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-motionmedia 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 secondsUpdating 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
}
Navigating Slides with Prev/Next Buttons
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: reducein 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:noneon all slides except one, as this can cause layout recalculation. Instead, usevisibility: hiddencombined withposition: absoluteandheight:0for non-visible slides, but always includearia-hidden="true". - Losing focus when sliding: After programmatically changing slides, ensure focus moves to the new slide or an appropriate control. The
focus()call ingoToSlideis crucial. - Overusing ARIA: Do not add ARIA roles or attributes that are already implicit in native HTML elements. For example, a
<button>already hasrole="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
touchstartandtouchendevents 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-labeland 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 usingaria-labelwith 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:
- WAI-ARIA Authoring Practices: Carousel Pattern – Official guidance from W3C on implementing accessible carousels.
- Accessible Rich Internet Applications (WAI-ARIA) 1.2 – The complete ARIA specification.
- web.dev Accessibility Learn Guide – Practical tutorials on accessibility fundamentals from Google.
- MDN: Using aria-live regions – Detailed reference for live region attributes.
- WAI: Evaluating Web Accessibility – Tools and methods for testing accessibility.
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.