# Motion and prefers-reduced-motion: animations that respect the visitor

> Which animations make people ill, what WCAG actually requires, the media query with both strategies, how GSAP, Framer Motion and Lottie honour the preference, and where motion also costs you speed.

Updated 2026-09-25 · Accessibility · HTML version: https://getreport.app/guides/motion-and-prefers-reduced-motion

Some people cannot look at a parallax hero or a zooming page transition without feeling dizzy or sick. Their operating system has a switch for that, "Reduce motion", and every current browser passes it to your CSS and JavaScript as `prefers-reduced-motion`. Honouring it is one media query and a few decisions about which animations to keep. This guide covers what the law and WCAG require, the two ways to write the query, what to reduce and what can stay, how the common animation libraries handle it, and the overlap with page speed, where motion also costs Core Web Vitals.

## Quick answer

- Add a global reduce block so every animation and transition becomes instant when the visitor asks:

```css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

- For new work, do the opposite: write animations inside `@media (prefers-reduced-motion: no-preference)` so the default is still.
- Anything that moves by itself for more than 5 s needs a visible pause button (WCAG 2.2.2, level A). Autoplaying background video counts.
- Nothing may flash more than three times a second (WCAG 2.3.1, level A).
- Reduce, do not delete: keep hover feedback and short fades; remove parallax, auto-advancing carousels, scroll-jacking and large zooms.
- Test with the OS switch or DevTools' rendering emulation, then run the [speed test](https://getreport.app/tools/speed-test): animations that move layout show up as CLS and long tasks.

## Why motion matters

Vestibular disorders, migraine, some forms of ADHD and plain motion sickness make certain screen movement physically unpleasant: the content moves while the eyes and inner ear report that the body is still. Triggers are large areas moving, movement tied to scrolling (parallax, scroll-jacking), zooms and spins, and anything that keeps going. Small, brief, user-initiated movement (a button that darkens on hover, a 150 ms fade) rarely bothers anyone. The affected group is a meaningful share of adults, and it includes people who never mention it and simply close the tab.

There is a second audience. Animation that runs while the page loads, or on every scroll, burns CPU on phones, and animation that changes layout (height, margin, top) forces the browser to re-lay out the page. Those show up as Cumulative Layout Shift and as long tasks on the main thread, which are speed findings with ranking consequences.

## How getReport checks it

> **Free tool:** [Accessibility checker (WCAG 2.2)](https://getreport.app/tools/accessibility-checker): axe-core violations by impact, colour contrast, landmarks, form labels, link names, tap targets and zoom — measured on the rendered page, with the selectors to fix.

Say it plainly: there is no automated check for reduced motion. axe-core, which the [accessibility checker](https://getreport.app/tools/accessibility-checker) runs on the rendered page, has no rule that detects animation, parallax, or whether the `prefers-reduced-motion` query exists in your CSS. The report cannot tell whether your site respects the preference. What it can measure are three side effects:

> **Check: No video starts playing automatically.** Autoplaying video downloads megabytes before the visitor asked for anything, drains phone batteries and makes the page move while people try to read it.
>
> 1. Remove the autoplay attribute and show a poster image with a play button instead.
> 2. If a background video must autoplay, keep it muted, under 1 MB and use prefers-reduced-motion to disable it.

This one counts `<video>` elements that carry the `autoplay` attribute in the rendered DOM (the [video autoplay](https://getreport.app/learn/video-autoplay) page has the thresholds). It does not see videos started by JavaScript, or embedded players from YouTube and Vimeo, so a pass here is not proof that nothing autoplays.

> **Check: Cumulative Layout Shift.** CLS measures how much the page jumps around while loading — the button that moves just as someone taps it. It is one of Google's three Core Web Vitals.
>
> 1. Give every <img>, <video>, ad slot and embed explicit width and height (or aspect-ratio) so space is reserved before it loads.
> 2. Preload the main web font and use font-display optional, or swap with a size-adjusted fallback font, so text does not reflow.
> 3. Never insert banners or content above existing content after load.

[CLS](https://getreport.app/learn/cumulative-layout-shift) is measured in the Lighthouse lab run. Entrance animations that slide content in from `margin-top: 40px`, banners that push the page down, and elements whose height animates on load all register as layout shift; so do the fonts and images the finding's fix mentions. An animation that only changes `transform` and `opacity` does not.

> **Check: No moderate accessibility violations.** Moderate issues slow visitors down rather than stopping them, for example headings out of order or regions without a name. Fixing them makes the page easier for everyone to scan.
>
> 1. Open the technical detail to see each rule and the elements affected.
> 2. Fix them in the shared layout or theme so every page benefits at once.

The moderate group is where the mechanical parts of a carousel or a video component tend to land: a `region` without a name, a scrollable slider that keyboard users cannot reach. The animation itself is invisible to it.

![The accessibility panel of a report on a page with many problems, showing the findings grouped by impact; none of them concerns animation, which is why the tests in this guide are manual](https://getreport.app/guides/img/motion-and-prefers-reduced-motion/panel.webp "Everything about motion in this guide is a manual check; the report covers the side effects.")

## What WCAG requires

The criteria people cite, and what each actually demands at level AA (the level the European Accessibility Act points to):

| Criterion | Level | What it requires |
| --- | --- | --- |
| 2.2.2 Pause, Stop, Hide | A | Moving, blinking or scrolling content that starts automatically, lasts more than 5 s and sits beside other content must have a way to pause, stop or hide it |
| 2.3.1 Three Flashes or Below Threshold | A | Nothing flashes more than three times in one second |
| 2.3.3 Animation from Interactions | AAA | Motion triggered by an action (scroll, click) can be turned off unless it is essential |

So at AA, the hard rules are a pause control for anything that moves by itself and no flashing. Respecting `prefers-reduced-motion` is how you meet 2.3.3 and it is the accepted way to meet the spirit of the rest; it is not, strictly, an AA requirement. Do it anyway: it is one media query.

## Step by step

### 1. Pick a strategy: opt-out or opt-in

**Opt-out (global reduce).** The snippet in the quick answer, placed at the end of your CSS. Every animation and transition finishes in 0.01 ms, which is effectively instant, and smooth scrolling becomes a jump. `0.01ms` rather than `0` because some JavaScript waits for `transitionend` and `animationend`, and a zero duration never fires those events in every browser. `animation-iteration-count: 1` stops infinite loaders spinning forever at 0.01 ms per cycle, which would peg the CPU.

The trade-off: it is blunt. A modal that fades in now snaps in; a loading spinner becomes a static icon, which is fine; a progress bar that animated to 60 % now jumps to 60 %, which is also fine. Its one real cost is that a state you only conveyed through motion ("the item flew into the cart") is now conveyed by nothing. Add text or a colour change for those.

**Opt-in.** Write the animation inside a `no-preference` query, so visitors who asked for reduced motion get the static version by default:

```css
.hero-image {
  opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
  .hero-image {
    animation: rise 600ms ease-out both;
  }
  @keyframes rise {
    from { opacity: 0; transform: translateY(16px); }
  }
}
```

This is the better pattern for new code and for the big effects. Most sites end up with both: the global block as a safety net, and opt-in for the effects you design deliberately.

### 2. Decide what to reduce and what to remove

Reduce to a fade or a static state:

- **Parallax** backgrounds and layers moving at different scroll speeds. Show the layers still.
- **Auto-advancing carousels.** Stop on the first slide; keep the arrows.
- **Background videos.** Show the poster frame.
- **Scroll-jacking** and scroll-snapped full-page sections with animated transitions. Plain scrolling.
- **Large zoom or slide transitions** between pages or views. A 150 ms cross-fade, or nothing.
- **Infinite loaders**, animated gradients, floating decorations. Static.

Can stay, even under `reduce`:

- Hover and focus feedback: colour, underline, a 1–2 px shift.
- Short opacity fades under 200 ms.
- Progress indicators that show state (a bar filling), ideally without easing flourishes.

The distinction is size and duration: a large part of the screen moving, or anything moving for seconds, goes; small and brief stays.

### 3. Give autoplaying video and carousels a pause button

WCAG 2.2.2 applies whether or not the visitor set a preference. A background video needs a visible control:

```html
<div class="hero">
  <video id="bg" autoplay muted loop playsinline poster="/img/hero.jpg" aria-hidden="true">
    <source src="https://getreport.app/video/hero.webm" type="video/webm">
    <source src="https://getreport.app/video/hero.mp4" type="video/mp4">
  </video>
  <button type="button" id="bg-toggle" aria-pressed="true">Pause video</button>
</div>
```

```js
const video = document.getElementById('bg');
const toggle = document.getElementById('bg-toggle');
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');

function setPlaying(playing) {
  if (playing) video.play(); else video.pause();
  toggle.textContent = playing ? 'Pause video' : 'Play video';
  toggle.setAttribute('aria-pressed', String(playing));
}

// Do not autoplay at all for visitors who asked for reduced motion
if (reduce.matches) {
  video.removeAttribute('autoplay');
  setPlaying(false);
}
toggle.addEventListener('click', () => setPlaying(video.paused));
reduce.addEventListener('change', (e) => { if (e.matches) setPlaying(false); });
```

The `poster` is what reduced-motion visitors see, so make it a real frame, not black. Keep the file small and the `muted` attribute; [Video on landing pages without killing LCP](https://getreport.app/guides/video-on-landing-pages-without-killing-lcp) covers the speed side.

A carousel that respects the preference:

```js
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
let timer = null;

function startAutoplay() {
  if (reduce.matches || timer) return;
  timer = setInterval(next, 6000);
}
function stopAutoplay() {
  clearInterval(timer);
  timer = null;
}

startAutoplay();
document.getElementById('carousel-pause').addEventListener('click', () => {
  timer ? stopAutoplay() : startAutoplay();
});
// Pause while the visitor interacts, per WCAG 2.2.2
carousel.addEventListener('mouseenter', stopAutoplay);
carousel.addEventListener('focusin', stopAutoplay);
```

### 4. Tell your animation library

- **GSAP**: `gsap.matchMedia()` runs a setup function only when a media query matches, and reverts its animations when it stops matching:

```js
const mm = gsap.matchMedia();
mm.add('(prefers-reduced-motion: no-preference)', () => {
  gsap.from('.card', { y: 24, opacity: 0, stagger: 0.1 });
});
```

- **Framer Motion / Motion**: the `useReducedMotion()` hook returns `true` when the preference is set, and `<MotionConfig reducedMotion="user">` makes every animation in the tree respect it, keeping opacity changes and dropping transforms.
- **Lottie**: the web player has no switch of its own. Check `matchMedia` before creating the animation and load it with `autoplay: false`, showing the first or last frame instead.
- **requestAnimationFrame loops** (particle backgrounds, custom scroll effects): wrap the start in the same `matchMedia` check, and stop the loop when the tab is hidden. A loop that runs at 60 frames a second on every page view is a long-task generator on phones.

### 5. Scroll-driven animations and view transitions

CSS scroll-driven animations (`animation-timeline: scroll()` or `view()`) are parallax by another name; put them inside `no-preference`. The View Transitions API animates between page states; disable its default cross-fade and any custom group animations under `reduce`:

```css
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
```

### 6. Test it

Turn the setting on and reload:

- **macOS**: System Settings → Accessibility → Display → Reduce motion.
- **iOS**: Settings → Accessibility → Motion → Reduce Motion.
- **Windows**: Settings → Accessibility → Visual effects → Animation effects off.
- **Android**: Settings → Accessibility → "Remove animations" (the label varies by manufacturer).
- **Chrome DevTools** without touching the OS: More tools → Rendering → "Emulate CSS media feature prefers-reduced-motion".

Then scroll the whole page, open the menu, open a modal, and watch for anything that still moves by itself.

### 7. Measure the speed side

Run the speed test on the same page. If the CLS finding names an element with an entrance animation, change the animation to `transform` and `opacity` only; those do not move layout. If the long-tasks finding lists your animation script, the loop is running too often or on too many elements; [Long tasks and main-thread work](https://getreport.app/guides/long-tasks-and-main-thread-work) has the diet. Fixing motion for accessibility and fixing it for speed are usually the same edit.

## Platform notes

**WordPress.** Elementor's Motion Effects and entrance animations, Divi's scroll effects and most theme "animate on scroll" options are CSS classes with JavaScript triggers; test the page with the OS setting on. If the effects still run, add the global reduce block under Appearance → Customize → Additional CSS (core), or Elementor's Site Settings → Custom CSS with Pro. Autoplaying hero videos in these builders usually have a "Play on mobile" toggle worth switching off, and a poster image field that should be filled.

**Webflow.** Interactions are JavaScript-driven; check them with the emulation and gate the big ones with a custom-code `matchMedia` check, or make the animated state the initial state so nothing moves when the script does not run.

**Squarespace and Wix.** Site-wide animation settings exist (Squarespace 7.1 has an Animations section in Site styles with a "None" option); you cannot add the media query to the platform's own effects, so prefer the least motion the theme allows and avoid auto-playing galleries.

**Static sites and custom.** Put the global block in your base stylesheet; pass `matchMedia` results into your components once and read them, rather than checking in every animation.

## Verify

- With Reduce motion on, nothing on the page moves without a click: no carousel advancing, no video, no parallax, no entrance animations.
- Every autoplaying element has a visible pause control that works with the keyboard.
- The speed test's CLS finding is at or below 0.1 and the video-autoplay finding passes (or lists only the video with a pause button).
- `document.getAnimations().length` in the console with the preference on is close to zero after load.

## Common mistakes

- **Setting `animation: none` globally.** Scripts that wait for `animationend` never continue; elements stuck at their "from" state stay invisible. Use `0.01ms` durations.
- **Hiding the carousel under `reduce` instead of stopping it.** The content disappears for exactly the people who asked for less movement. Show the first slide with arrows.
- **Autoplay video with no poster.** Reduced-motion visitors get a black rectangle. Provide a real frame.
- **A pause button that only appears on hover.** Keyboard and touch users cannot reach it. Make it always visible or visible on focus.
- **Treating the OS switch as the only signal.** Most visitors never find that setting. Keep the large effects short and optional for everyone, not only for those who opted out.
