Providing Pause Controls for Autoplay Motion

Part of WCAG Animation Conformance in Accessible Motion Architecture.

The problem

A hero carousel rotates every five seconds. A ticker scrolls headlines continuously. A background animation loops behind the copy. Each of them starts on its own, none of them stops, and all of them sit next to text that someone is trying to read.

Success criterion 2.2.2 is unusually concrete about this: for moving content that starts automatically, lasts more than five seconds, and is presented alongside other content, there must be a mechanism to pause, stop or hide it. It is a Level A criterion, which puts it in the baseline rather than the aspirational tier — and the mechanism most implementations ship, pausing on hover, does not satisfy it.

Root cause: hover is not a mechanism

Pausing on hover feels like a control because it works for the person who built it, using a mouse, on a desktop. It fails for everyone else.

A keyboard user has no hover. A touch user has no hover in any sustained sense. A screen reader user is not moving a pointer over the carousel at all. And even for a mouse user, hover is not a choice — the motion resumes the moment the pointer leaves, which means the only way to read the adjacent text in peace is to leave the pointer parked over the carousel while reading elsewhere.

The criterion asks for a mechanism, and a mechanism has three properties hover lacks: it is reachable by any input method, it is discoverable, and its effect persists until the user changes it. In practice that is a button.

The second half of the problem is what “pause” means technically. Removing the animation class stops the motion by resetting it, which loses the user’s place — a carousel that jumps back to slide one is not paused. Pausing has to hold the current frame, which is what animation-play-state: paused and the animation object’s pause() both do.

Which mechanisms satisfy the criterionOnly a persistent, input-agnostic control qualifies.Which mechanisms satisfy the criterionKeyboard reachablePersistsSatisfies 2.2.2Pause on hoverNoNoNoPause on focus onlyYesNoNoVisible pause buttonYesYesYesSetting in apreferences panelYesYesYes, if discoverableRespectingprefers-reduced-motionn/aYesHelps, not sufficientalone
Only a persistent, input-agnostic control qualifies.

Step-by-step resolution

Building the controlThe button is the easy part; holding the frame and announcing the state are where implementations slip.Building the control1Check the three conditions: automatic start, over five seconds, alongsideother content.All three must hold for the criterion to apply2Add a visible button inside the component, in the tab order, before themoving content.Discoverable by keyboard before the motion is encountered3Pause by holding the frame with animation-play-state, not by removing theanimation.The carousel keeps its place4Update the accessible name and aria-pressed when the state changes.Screen reader users hear the new state5Start paused when prefers-reduced-motion is set.No autoplay for users who asked for less motion
The button is the easy part; holding the frame and announcing the state are where implementations slip.

Production code pattern

<!-- The control comes before the moving content in the DOM, so a keyboard
     user reaches it before tabbing into the carousel. -->
<section class="ticker" data-playing="true">
  <button class="ticker__toggle" type="button" aria-pressed="false"
          aria-label="Pause the news ticker" data-ticker-toggle>
    <span aria-hidden="true"></span>
  </button>
  <div class="ticker__track"></div>
</section>
/* Intent: pausing holds the current frame rather than resetting the motion,
   and the whole thing starts paused when the user has asked for less motion. */
.ticker__track {
  animation: ticker-scroll 22s linear infinite;
}

/* One attribute controls the play state — no class churn, no reset. */
.ticker[data-playing='false'] .ticker__track {
  animation-play-state: paused;
}

@keyframes ticker-scroll {
  to { translate: -50% 0; }
}

/* Accessibility gate: continuous motion is exactly what the preference asks
   to be removed, so autoplay does not start at all. */
@media (prefers-reduced-motion: reduce) {
  .ticker__track { animation-play-state: paused; }
}
// Intent: one attribute is the source of truth; the button reflects it.
const ticker = document.querySelector('.ticker');
const toggle = ticker.querySelector('[data-ticker-toggle]');

// Respect the preference on first render, before anything moves.
const prefersReduced = matchMedia('(prefers-reduced-motion: reduce)');
if (prefersReduced.matches) ticker.dataset.playing = 'false';

function sync() {
  const playing = ticker.dataset.playing === 'true';
  toggle.setAttribute('aria-pressed', playing ? 'false' : 'true');
  toggle.setAttribute('aria-label', playing ? 'Pause the news ticker'
                                            : 'Play the news ticker');
}

toggle.addEventListener('click', () => {
  ticker.dataset.playing = ticker.dataset.playing === 'true' ? 'false' : 'true';
  sync();
});

// A mid-session preference change stops the motion immediately.
prefersReduced.addEventListener('change', (event) => {
  if (event.matches) ticker.dataset.playing = 'false';
  sync();
});

sync();

Rendering Impact: translate — composite only, and animation-play-state is a style change that freezes the compositor animation in place rather than tearing it down. Pausing costs one style pass, not a re-layout.

The aria-pressed semantics are worth pausing on, so to speak. The button is a toggle, and its pressed state describes whether the pause is engaged — so when the ticker is playing, the pause button is not pressed. Getting this inverted is the most common accessibility bug in otherwise correct implementations, and it makes the announcement say the opposite of the truth.

Auditing an existing autoplaying componentFive checks that take about two minutes each.Auditing an existing autoplaying component1Time the motion: does it stop by itself within five seconds?If not, the criterion applies2Tab to the component and try to stop it without a mouse.A hover-only pause fails here3Pause it and confirm it holds position rather than resetting.Resetting loses the user's place4Listen to the control with a screen reader in both states.The name and pressed state must match reality5Emulate reduced motion and reload.Autoplay should not begin
Five checks that take about two minutes each.

Verification checklist

Constraints and trade-offs

  • A pause control adds a permanent piece of interface to the component, which is one more argument for not autoplaying in the first place.
  • animation-play-state freezes the frame but leaves the layer promoted; long-paused ambient motion should be demoted as well.
  • Starting paused under reduced motion means some users never see the additional slides, so the content must not be exclusive to the carousel.
  • A preferences-panel setting can satisfy the criterion only if users can find it, which usually means an in-context control anyway.
  • Pausing must survive re-renders; a framework that recreates the element resets the animation unless the state attribute is part of its data.

Frequently asked questions

Does pausing on hover satisfy 2.2.2?

No. Hover is not available to keyboard or touch users, and it is not a persistent choice. The criterion asks for a mechanism to pause, stop or hide the content, which means a real control that any input method can reach and that keeps its state.

Does the five-second rule mean short animations are exempt?

Motion that stops on its own within five seconds is outside 2.2.2, yes. That exemption is narrower than it sounds: a three-second animation that loops indefinitely has not stopped, so it is in scope.

Should the carousel autoplay at all?

Usually not. Autoplay is the source of the requirement, and removing it removes the requirement along with a documented usability problem. Where a product decision keeps it, the control is mandatory rather than a nicety.


Where the control should live, visually

A conformant control that nobody can find satisfies the letter of the criterion and not its purpose. Three placement decisions decide whether it is genuinely usable.

Inside the component, not in a settings panel. A user bothered by a rotating carousel is looking at the carousel; a preference buried two menus away is not a mechanism they will discover mid-sentence. If a global setting exists as well, treat it as an addition rather than a replacement.

Before the moving content in the DOM. Tab order follows the DOM, so a control placed after the carousel is reached only by tabbing through the thing the user is trying to stop. Placing it first means the first Tab press into the region offers the escape hatch.

Visible without interaction. A control revealed on hover is invisible to the people most likely to need it, and a control that fades in on focus is invisible until focus is already there. Persistent and low-contrast beats hidden and prominent.

The visual weight is a genuine design tension: the control is important but should not compete with the content. A small, permanently visible icon button with an accessible name and a 44 pixel touch target resolves it in most designs — and if the component’s design cannot accommodate one, that is a strong signal the content should not be autoplaying in the first place.