prefers-reduced-motion: Meaning and How to Emulate It

prefers-reduced-motion is the CSS media feature for users who want less animation. What it does, why it makes screenshots repeatable, and tested code.

By Simon Chiu · Updated

prefers-reduced-motion is a CSS media feature that reports whether the user asked the operating system to minimise non-essential motion, so a page can replace animations with an instant, static result.

It exists for accessibility: large movement on screen can cause nausea or dizziness for people with vestibular disorders. For anyone who captures pages, it has a second use. A page that honours it stops moving, and a page that stops moving gives the same screenshot every time. This entry is part of the screenshot glossary.

What it means in plain terms

macOS, iOS, Windows and Android all have a "reduce motion" or "show animations" switch. The browser passes that choice to every page as prefers-reduced-motion: reduce or no-preference. A well-built site has CSS that says "if the reader prefers less motion, skip the fade-in and show the final state". A site without that CSS animates either way.

A headless browser has no person and no system switch, so it reports no-preference unless you set the value. Entrance animations then play during your capture, and the screenshot shows whatever frame the clock happened to reach.

The same page, both values, 300 ms after load

We built a test hero with two entrance animations: a heading that rises and fades in over 1.2 seconds, and a bar that grows over 1.5 seconds. Its CSS has one reduced-motion rule that turns both animations off.

The test hero captured with no-preference: the heading is faded and below its final position, and the blue bar is about one third of its full width The same hero captured with reduce: the heading is fully visible in its final position and the blue bar has its full width
The test page at 1200×630, captured 300 ms after the load event with prefers-reduced-motion emulated as no-preference (top) and reduce (bottom). Puppeteer 25.14.0 and headless Chrome 155, 2026-10-15, scaled to 600×315 WebP.

To put a number on "repeatable", we captured the page twice per value, once 300 ms and once 600 ms after load, and compared the two images with pixelmatch at threshold 0.1:

Emulated value Pixels that differ between the 300 ms and 600 ms captures
no-preference 8,239 (8,240 on a second run)
reduce 0 (0 on a second run)

With motion allowed, the result depends on timing. With motion reduced, it does not. That is the property a pixel diff in a visual test needs.

How it works technically

The page reads the value in CSS:

.hero-title { animation: rise 1.2s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  .hero-title { animation: none; }
}

or in JavaScript, with matchMedia('(prefers-reduced-motion: reduce)').matches. Animation libraries often check it once at start-up, so the emulated value must be in place before the page loads. Setting it after navigation fixes the CSS, but a script that already read false keeps animating.

animation: none is the important detail on the page side. A rule that only shortens the duration still leaves a first frame, and some sites do exactly that.

How to set it

Puppeteer

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 630 });
  // Before goto, so the first style calculation and start-up scripts see it.
  await page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]);
  await page.goto('https://example.com', { waitUntil: 'load' });

  console.log('reduce =', await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches));
  await page.screenshot({ path: 'still.png' });
} finally {
  await browser.close();
}

Playwright

import { chromium } from 'playwright';

const browser = await chromium.launch();
try {
  // reducedMotion is a context option: every page in the context gets it.
  const context = await browser.newContext({ reducedMotion: 'reduce', viewport: { width: 1200, height: 630 } });
  const page = await context.newPage();
  await page.goto('https://example.com');
  console.log('reduce =', await page.evaluate(() => matchMedia('(prefers-reduced-motion: reduce)').matches));
  await page.screenshot({ path: 'still-pw.png' });

  // Switch on the same page without a new context.
  await page.emulateMedia({ reducedMotion: 'no-preference' });
} finally {
  await browser.close();
}

Tested on 2026-10-15 with Node 22.18.0, [email protected] (Chrome 155) and [email protected] (Chromium 156), against the test hero above and example.com. Both printed true for the reduce check, and Playwright reported false after the switch.

Playwright Test also has animations: 'disabled' on toHaveScreenshot. That option fast-forwards CSS animations and transitions in the page itself, so it works even on sites that ignore the media feature. Puppeteer has no equivalent; inject *, *::before, *::after { animation: none !important; transition: none !important; } with page.addStyleTag if you need it.

RenderScreenshot

Set browser.reduced_motion to true in a POST request. The renderer applies it before navigation, together with the colour scheme:

curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "preset": "og_card", "browser": {"reduced_motion": true}}' \
  -o still.png

reduced_motion is a POST option; the flat GET parameters do not include it. For sites that animate regardless, add the CSS rule above through page.styles, or a wait.delay. See browser options for the full list.

Common mistakes

  • Setting it after goto. CSS reacts, but a library that read the value on load keeps its first answer.
  • Treating it as a freeze button. It is a request. JavaScript carousels, canvas and video often ignore it.
  • Removing animations for real users. Emulate the preference in the capture only. Do not ship CSS that disables motion for everyone.
  • Mixing values across baselines. A baseline captured with motion and a test run without it differ on every animated page.

To capture your own page with motion reduced, get a free API key and run the request above.

Frequently asked questions

What are the values of prefers-reduced-motion?
Two: no-preference, the default, and reduce. A page decides what reduce means. Most sites shorten or remove CSS transitions and animations, and some stop autoplaying carousels and videos.
Does emulating reduced motion stop every animation?
No. It only changes what the page reads. Animations that do not check the media feature, such as many JavaScript sliders, canvas effects and GIFs, keep moving. Add a short wait or hide those elements as well.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.