Accessibility8 min read

Animated WebP and Accessibility: Respecting Reduced Motion

An animated WebP starts playing the moment it renders, loops forever, and offers no pause button. Here is how to ship one responsibly — without trapping motion-sensitive users in an animation they cannot stop.

The problem: motion you cannot turn off

Animated WebP inherits GIF's playback model exactly. When the browser decodes the file, it starts the animation immediately, loops it according to the loop count baked into the container (usually infinite), and exposes no JavaScript API to pause, scrub, or restart it. Unlike a <video> element, there are no controls, no pause() method, and no way for the user to make it stop short of leaving the page.

For most people this is a non-issue. But persistent, looping motion is a documented accessibility barrier. Users with vestibular disorders can experience nausea, dizziness, and disorientation from movement in their peripheral vision. Users with ADHD or other attention differences can find a looping animation impossible to read around — it pulls focus on every cycle. A spinning logo or a parallax loop that you find delightful can make a page genuinely unusable for someone else.

This is why WCAG Success Criterion 2.2.2 (Pause, Stop, Hide) exists. It states that for any moving, blinking, or scrolling content that starts automatically, lasts more than five seconds, and is presented alongside other content, the user must have a way to pause, stop, or hide it. An infinitely looping animated WebP runs well past five seconds by definition, so an autoplaying WebP next to your article text is squarely in scope. The spirit of the 2.3.x criteria (which target seizure and physical-reaction risks from flashing) reinforces the same principle: do not force motion on people who did not ask for it.

Why CSS prefers-reduced-motion alone cannot stop a WebP

Most operating systems expose a "reduce motion" accessibility toggle, and browsers surface it to the web through the prefers-reduced-motion media query. It is the right signal to listen for. But it is easy to assume that wrapping your animation rules in a media query is enough — and for an animated image, it is not.

The reason is what prefers-reduced-motion actually governs. It controls CSS transitions and animations, and any JavaScript-driven motion you choose to gate on it. It does not govern the internal playback of an animated image. When the browser encounters an <img> pointing at an animated WebP, the image decoder runs the animation as part of rendering the element. That is a property of the media itself, not of any CSS animation declared on the element. There is no animation-play-state that touches it.

So the following does nothing useful for a WebP, even though it reads like it should:

/* This does NOT stop an animated WebP. */
@media (prefers-reduced-motion: reduce) {
  .my-image {
    animation: none;          /* there is no CSS animation to cancel */
    animation-play-state: paused;
  }
}

The fix is to stop serving the motion in the first place when the preference is set — swap the animated file for a static one. There are three reliable ways to do that, in increasing order of effort and control.

Solution 1: <picture> with a reduced-motion source

The cleanest solution needs no JavaScript at all. The <picture> element lets you list candidate sources with media conditions; the browser picks the first <source> whose media query matches and falls back to the <img> otherwise.

<picture>
  <!-- Served only when the user has asked to reduce motion -->
  <source
    srcset="demo-static.png"
    media="(prefers-reduced-motion: reduce)"
  />
  <!-- Default for everyone else -->
  <img
    src="demo-animated.webp"
    alt="Drag a clip onto the converter and a WebP appears in seconds"
    width="640"
    height="360"
  />
</picture>

How the browser resolves this: it evaluates each <source> top to bottom. If the user has reduce-motion enabled, the first source matches and the static PNG is loaded — the animated WebP is never even fetched. If the preference is not set, the source is skipped and the browser falls through to the <img>, which serves the animation. The alt, width, and height always come from the <img>, so set them there.

This is the right default for decorative or short looping content. It is declarative, has zero runtime cost, degrades gracefully on old browsers (they ignore the source and show the WebP), and it updates live — if the user flips the OS setting, the browser re-evaluates and swaps the image without a reload.

Solution 2: JavaScript swap with matchMedia

When you need more control — for example, the image is generated dynamically, or you want to combine the preference with other logic — read the same media query from JavaScript with window.matchMedia and set the src yourself. The important detail most snippets get wrong: listen for changes too, so the image responds when the user toggles the setting while the page is open.

const img = document.querySelector('#hero');
const animated = 'demo-animated.webp';
const still = 'demo-static.png';
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');

function applyPreference(reduce) {
  img.src = reduce ? still : animated;
}

// Set the correct source on first load.
applyPreference(mq.matches);

// React when the user changes the OS setting mid-session.
mq.addEventListener('change', (event) => {
  applyPreference(event.matches);
});

Two notes on robustness. First, this runs only after JavaScript executes, so for a fraction of a second a reduce-motion user could see the animated frame if you ship the animated src in the HTML. Avoid that by defaulting the markup to the static image and only upgrading to the animation when the preference is absent — or just prefer the <picture> approach above, which has no flash window.

Second, addEventListener('change', ...) on a MediaQueryList is supported everywhere that supports animated WebP, so you do not need the old addListener fallback.

Solution 3: click-to-play for motion longer than five seconds

Reduce-motion handling is the floor, not the ceiling. WCAG 2.2.2 applies to everyone, not only people who have set a preference — if your animation auto-plays, loops, and runs longer than five seconds, you owe all users a way to stop it. Reduce-motion users opted out; the rest never had a control to begin with. The standard pattern is click-to-play: show a static poster with a play button, and swap in the animation only when the user asks for it.

<button id="player" class="poster" aria-pressed="false">
  <img id="frame" src="demo-static.png"
       alt="Conversion demo — press to play" width="640" height="360" />
  <span class="play-badge" aria-hidden="true">▶</span>
</button>

<script>
  const btn = document.querySelector('#player');
  const frame = document.querySelector('#frame');
  let playing = false;
  btn.addEventListener('click', () => {
    playing = !playing;
    frame.src = playing ? 'demo-animated.webp' : 'demo-static.png';
    btn.setAttribute('aria-pressed', String(playing));
    frame.alt = playing
      ? 'Conversion demo — press to pause'
      : 'Conversion demo — press to play';
  });
</script>

Because the page renders with the static poster and the user initiates motion deliberately, this satisfies WCAG 2.2.2 cleanly: nothing auto-plays, and the same control that starts the animation stops it. The aria-pressed state and the updated alt keep the toggle understandable to screen-reader users. Use a real <button> so it is keyboard-focusable and operable with Enter or Space for free. You can layer this on top of the reduced-motion logic above: serve the poster by default, and only auto-play when both the user presses play and reduce-motion is off.

Exporting the still frame to use as a poster

All three solutions need a static image. The best poster is usually the first frame of the animation, so the static and animated versions line up pixel-for-pixel and there is no jump when the user starts playback. If you are converting with FFmpeg, grab frame zero like this:

# Pull the first frame of the source clip as a PNG poster
ffmpeg -i input.mp4 -vframes 1 demo-static.png

# Or extract the first frame directly from a finished WebP
ffmpeg -i demo-animated.webp -vframes 1 demo-static.png

# Match dimensions to the animated output (e.g. 640 wide)
ffmpeg -i input.mp4 -vframes 1 -vf "scale=640:-1" demo-static.png

The 2WebP converter makes both halves of this in one place. It produces the animated WebP entirely in your browser, and because it renders frames client-side you can grab a representative still to use as your poster — no command line required. Start at the converter, and if you are still weighing the format itself, the WebP vs GIF comparison covers why animated WebP usually wins on size and color.

Alt text for animated content

An animated image is still an image as far as assistive technology is concerned, and the same alt rules apply — with one twist: you are describing a sequence, not a single frame. Summarize the outcome or the point of the motion, not every frame. "A clip being dragged onto the converter, then a WebP file appearing" tells a screen-reader user what they are missing far better than "animation" or a frame-by-frame play-by-play.

  • If the animation is decorative — it conveys no information the surrounding text does not — give it an empty alt="" so screen readers skip it.
  • If it demonstrates a process, describe the start state, the change, and the end state in one sentence.
  • In a click-to-play control, let the altdouble as the affordance — "…press to play" / "…press to pause" — and reflect state with aria-pressed.
  • For anything genuinely complex, put a fuller description in nearby visible text or a caption rather than overloading alt.

The accessibility checklist

Before you ship an animated WebP to a general audience, run through this:

  • Static fallback exists. You have a still frame (ideally frame one) exported at the same dimensions as the animation.
  • Reduced motion is honored. Reduce-motion users get the static image via <picture> or a matchMedia swap — not a CSS-only rule, which does nothing here.
  • Live updates work. Flipping the OS setting while the page is open swaps the image without a reload.
  • Long motion is pausable. If the animation auto-plays, loops, and runs past five seconds beside other content, there is a control to pause, stop, or hide it — satisfying WCAG 2.2.2. Click-to-play covers this for everyone.
  • No flashing. The content does not flash more than three times per second, keeping you clear of the 2.3.x seizure criteria.
  • Alt text describes the sequence (or is empty for decorative motion), and interactive players expose their state.
  • Keyboard operable. Any play control is a real focusable element, usable with Enter or Space.

None of this means avoiding animation — motion is a powerful way to show a process or add life to a page. It means giving people the choice. For the markup mechanics of embedding the file itself, see embedding animated WebP in HTML.

Make the animated WebP and grab a still poster in the same place — try the converter — everything runs in your browser, so your file never leaves your device.