The author discusses accessibility in-depth in the article "How to build a more accessible carousel or slider", with examples in vanilla JavaScript. It helps me a lot; please read it to get a thorough understanding. In the article below, I demonstrated how to create an accessible slideshow with React.
Or read the entire code on CodeSandbox.
Here's the screenshot of the example slideshow:
This is a simple one. The slideshow only has two buttons to navigate to the previous or the next slide. Every slide has one focusable element (a
link).
I wrote the HTML(JSX) with some ARIA attributes at first.
<section aria-labelledby="carouselheading">
<h2 id="carouselheading">The List of "The Matrix" Series</h2>
{/* Add some introductions for this slideshow and visually hide it. */}
<p className="sr-only">
Carousel with one slide shown at a time. Use the Previous and Next buttons
to navigate.
</p>
{/* The Previous button */}
<button className="btn nav prev">
<span className="sr-only">Previous One</span>
<ChevronLeft size={64} />
</button>
{/* The group of slides */}
<div className="slides">
{MOVIES.map((movie, index) => (
<div
className="slide"
key={movie.id}
role="group"
aria-label={`slide ${index + 1} of ${MOVIES.length}`}
style={{
'--translateX': `-${slideIndex * 100}%`,
}}
>
<article>
<h3>{movie.title}</h3>
<p className="year">{movie.release_year}</p>
<p className="director">
Directed by <em>{movie.director}</em>
</p>
<a className="btn btn-primary" href={movie.link}>
Details
</a>
</article>
<figure>
<img src={movie.poster} alt={`${movie.title} poster`} />
</figure>
</div>
))}
</div>
{/* The Next button */}
<button className="btn nav next">
<span className="sr-only">Next One</span>
<ChevronRight size={64} />
</button>
</section>
View the whole code on CodeSandbox
Although I used SVG icons for the previous and next buttons, I also wrote the text and wrapped it in elements labeled with sr-only
classes to visually hide the text for accessibility.
For now, it is semantically accessible but not practical when used with a keyboard.
When we walk through the document with keyboard, the focus order depends on the order of the DOM. So, we need JavaScript to change the tabindex
of the visually hidden slide items, also add aria-hidden
to them from the accessibility devices. In this React app, I used useRef
to access those div
slide items.
const App = () => {
const slidesRef = useRef([]);
const makeSlidesAccessible = () => {
// Hiding all the slides and their content
slidesRef.current.forEach((slide) => {
if (slide) {
slide.setAttribute("aria-hidden", "true");
slide.querySelector("a").setAttribute("tabindex", "-1");
}
});
};
useEffect(() => {
makeSlidesAccessible();
}, []);
return (
{/* omit */}
{MOVIES.map((movie, index) => (
<div
ref={(element) => slidesRef.current.push(element)}
className="slide"
key={movie.id}
role="group"
aria-label={`slide ${index + 1} of ${MOVIES.length}`}
style={{
"--translateX": `-${slideIndex * 100}%`
}}
>
{/* omit */}
</div>
))}
{/* omit */}
);
};
In the code above, besides setting aria-hidden
to "true"
for all the slide items, I made every a
link within those items "unfocusable" by setting tabindex
to "-1"
to prevent the keyboard event from focusing on any hidden slides.
Next, by removing those added attributes, I made the current slide "visible" to accessibility devices and the a
link focusable again.
const App = () => {
const [slideIndex, setSlideIndex] = useState(0);
const slidesRef = useRef([]);
const makeSlidesAccessible = () => {
// Hiding all the projects and their content
slidesRef.current.forEach((slide) => {
if (slide) {
slide.setAttribute('aria-hidden', 'true');
slide.querySelector('a').setAttribute('tabindex', '-1');
}
});
// Make sure the current slide not hide
slidesRef.current[slideIndex].removeAttribute('aria-hidden');
slidesRef.current[slideIndex]
.querySelector('a')
.removeAttribute('tabindex');
};
useEffect(() => {
makeSlidesAccessible();
}, [slideIndex]);
return {
/* omit */
};
};
Then, everything works as expected.