Carousels
Auto-rotating content with slide navigation — one of the most accessibility-challenged patterns on the web
Overview
Carousels can create problems when users lose context, cannot reach controls, or cannot pause movement. WCAG 2.2.2 applies when moving content starts automatically, lasts more than five seconds, and appears in parallel with other content, unless the movement is essential. Auto-updating information has a related but distinct condition. A visible pause control is the most robust design for an auto-rotating carousel.
WCAG Criteria:
- 2.2.2 Pause, Stop, Hide — pause, stop, or hide qualifying non-essential movement
- 1.3.1 Info and Relationships — expose slide structure and relationships
- 4.1.2 Name, Role, Value — controls and regions need valid names, roles, and states
Key requirements:
- Auto-rotating content must have a visible Pause/Play button
- Carousel container and slides need appropriate ARIA roles (
aria-roledescription) - Slide position must be announced (e.g. “Slide 1 of 3”)
- Auto-rotation must pause on hover and on keyboard focus
- Respect
prefers-reduced-motion: reduceby disabling auto-play entirely - Tab order should flow logically: Previous, slide content, Next, Pause
- Consider whether a carousel is even the right pattern for your content
Carousel
Inaccessible Auto-Play vs. Accessible Carousel
Inaccessible
Slide 1
View inaccessible code
<!-- Auto-playing carousel — no pause, no ARIA, dot-only navigation -->
<div class="carousel">
<div class="slide" style="display:flex;">Slide 1</div>
<div class="slide" style="display:none;">Slide 2</div>
<div class="slide" style="display:none;">Slide 3</div>
<!-- Dot indicators only — no labels, not focusable -->
<div class="dots">
<span class="dot active"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
<script>
// Auto-rotates every 3 seconds — no way to pause
let current = 0;
setInterval(() => {
current = (current + 1) % 3;
slides.forEach((s, i) =>
s.style.display = i === current ? 'flex' : 'none'
);
}, 3000);
</script>Accessible
Slide 1
Slide 1 of 3
View accessible code
<!-- Accessible carousel with ARIA, pause, and keyboard support -->
<div
role="region"
aria-roledescription="carousel"
aria-label="Featured content"
onmouseenter="pause()"
onmouseleave="resumeIfNotUserPaused()"
onfocusin="pause()"
onfocusout="resumeIfNotUserPaused()"
>
<button aria-label="Previous slide" onclick="prevSlide()">
‹
</button>
<div role="group" aria-roledescription="slide" aria-label="1 of 3">
Slide 1
</div>
<div role="group" aria-roledescription="slide" aria-label="2 of 3" hidden>
Slide 2
</div>
<div role="group" aria-roledescription="slide" aria-label="3 of 3" hidden>
Slide 3
</div>
<button aria-label="Next slide" onclick="nextSlide()">
›
</button>
<button
aria-label="Pause auto-rotation"
onclick="togglePause()"
>
Pause
</button>
<!-- Live region announces slide changes -->
<div aria-live="polite" aria-atomic="true"
class="visually-hidden">
Slide 1 of 3
</div>
</div>
<script>
// Respects prefers-reduced-motion
const reducedMotion =
matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reducedMotion) {
// Don't auto-rotate at all
userPaused = true;
}
</script>What’s wrong with the inaccessible carousel?
- Auto-rotates every 3 seconds with no pause button — violates WCAG 2.2.2
- Navigation is limited to small dot indicators that are
<span>elements — not focusable, no labels, unreachable by keyboard - No ARIA roles — screen readers cannot identify the component as a carousel or individual slides
- No slide position information — users have no idea where they are in the sequence
- No way to stop or slow the auto-rotation for users who need more reading time
- Continues rotating even when the user is trying to interact with the content
What the accessible carousel provides:
aria-roledescription="carousel"on the container identifies the component to screen readers- Each slide has
role="group",aria-roledescription="slide", andaria-label="X of 3"for position info - Previous/Next buttons provide keyboard-accessible navigation with clear labels
- Pause/Play button satisfies WCAG 2.2.2 — users can stop auto-rotation at any time
aria-live="polite"region announces the current slide position during manual navigation- Auto-rotation pauses on hover (
onmouseenter) and keyboard focus (onfocusin) - Respects
prefers-reduced-motion: reduceby disabling auto-play entirely
Expected semantics:
| Version | Programmatic result |
|---|---|
| Inaccessible carousel | Exposes slide text without a carousel region, position, or operable controls. |
| Accessible carousel | Exposes a named region with the carousel role description, grouped slides with position labels, and named Previous, Next, and Pause actions. |
Key Teaching Points
- Apply WCAG 2.2.2 precisely — qualifying non-essential movement needs a pause, stop, or hide mechanism; record how the five-second and parallel-presentation conditions apply.
- Respect
prefers-reduced-motion: reduce— users who enable this OS-level setting are telling you they need reduced or no animation. Disable auto-rotation entirely for these users. - Tab order matters — the logical flow should be: Previous button, slide content (if interactive), Next button, Pause button. This keeps navigation predictable.
aria-roledescriptionprovides context — using “carousel” on the container and “slide” on each panel tells screen reader users what type of component they are interacting with, without overriding the base semantics.- Consider whether a carousel is even necessary — studies consistently show that users rarely interact with carousel content beyond the first slide. A static hero or a simple list may be more effective and far easier to make accessible.