Rotating banners, live arrival boards and looping animations need a pause, stop or hide control. Here is how to meet WCAG 2.2.2 on public sites and apps.
A rotating banner on a city homepage, a transit board that refreshes every few seconds, or a looping weather animation can make a page unreadable for people who read slowly, have attention difficulties or use a screen magnifier. Moving text is hard to follow, and constant change pulls attention away from the task.
Success Criterion 2.2.2 has two parts.
Both parts have an exception when the movement or update is essential to the activity. Content started by a deliberate user action, such as pressing a Play button, does not count as starting automatically. Flashing is a separate matter and falls under 2.3.1.
Alerts matter most here. A rotating banner that carries a road closure notice will often move on before a slow reader has finished it, so the notice also belongs somewhere static.
A board that re-renders every ten seconds is auto-updating content. Give users a pause control, and a manual refresh for when they want the latest times.
setInterval(loadArrivals, 10000);let timer = setInterval(loadArrivals, 10000);
pauseBtn.addEventListener("click", () => {
const paused = pauseBtn.getAttribute("aria-pressed") === "true";
pauseBtn.setAttribute("aria-pressed", String(!paused));
if (paused) timer = setInterval(loadArrivals, 10000);
else clearInterval(timer);
});
refreshBtn.addEventListener("click", loadArrivals);The pause button must itself be reachable by keyboard and must not move or change while the board updates.
A slider that advances on a timer needs a pause control. It is also good practice to start paused when the visitor has asked for reduced motion, but that preference does not replace the control.
setInterval(nextSlide, 4000);const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
let playing = !reduce;
setInterval(() => { if (playing) nextSlide(); }, 4000);
playBtn.addEventListener("click", () => {
playing = !playing;
playBtn.textContent = playing ? "Pause slides" : "Play slides";
});
playBtn.textContent = playing ? "Pause slides" : "Play slides";Moving content started by a deliberate action is outside the criterion. A radar loop can be shown as a still image with a button.
<img src="radar-loop.gif" alt="Radar loop, last two hours"><figure>
<img id="radar" src="radar-latest.jpg" alt="Radar, latest image">
<button type="button" onclick="playRadarLoop()">
Play two-hour loop
</button>
</figure>Onboarding carousels and rotating promotions in mobile apps count too. SwiftUI:
TabView(selection: $page) { /* promo cards */ }
.onReceive(timer) { _ in page = (page + 1) % cards.count }@Environment(\.accessibilityReduceMotion) var reduceMotion
@State private var autoPlay = true
TabView(selection: $page) { /* promo cards */ }
.onReceive(timer) { _ in
if autoPlay && !reduceMotion { page = (page + 1) % cards.count }
}
Button(autoPlay ? "Pause" : "Play") { autoPlay.toggle() }Jetpack Compose:
LaunchedEffect(Unit) {
while (true) { delay(4000); pager.animateScrollToPage(next()) }
}var autoPlay by remember { mutableStateOf(true) }
LaunchedEffect(autoPlay) {
while (autoPlay) { delay(4000); pager.animateScrollToPage(next()) }
}
TextButton(onClick = { autoPlay = !autoPlay }) {
Text(if (autoPlay) "Pause" else "Play")
}Automated tools rarely spot carousels, so a person has to look.
prefers-reduced-motion alone, with no visible control.Ship it when everything that starts by itself and keeps running past five seconds, or updates by itself, has a keyboard-operable pause, stop or hide control, or the update frequency is adjustable.
DOJ guidance on web accessibility is covered under common barriers.