Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 7.3Intermediate ~17 min · shot 3 of 7

Reduced Motion in JavaScript

In one line

CSS media queries do not reach animation you start from JavaScript. Read the setting with matchMedia, choose calm keyframes or skip the animation, and listen for the setting changing while the page is open.

0/7 · 0%

Think of it like this

A hotel puts a "do not disturb" card on a door. The card only works if the staff look at it each time before they knock. A media query in CSS looks at the card for you. In JavaScript, you must look at it yourself before every animation you start.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

matchMedia
A browser function that tests a media query from JavaScript and returns a MediaQueryList.
MediaQueryList
The object matchMedia returns. matches says whether the query is true now. It fires a change event when that changes.
end state
How things look when an animation has finished.
client hint
A request header where the browser tells the server something about the device or the user's settings.

Step by step

01Find every place code starts motion

Search for .animate(, requestAnimationFrame, setInterval that changes styles, and library calls such as gsap.to or animate(.

02Ask once per animation

Call matchMedia(...) where the animation starts, or use the shared helper from Topic 7.7. Choose calm keyframes or jump to the end state.

03Handle loops

Anything that repeats needs a change listener, or it keeps going after the visitor turns the setting on. Pause it (Topic 7.4 explains why anything over five seconds needs a pause anyway).

04Test both values

Run with the timeline's Reduced motion switch on and off. In real browsers, emulate the media feature in the DevTools Rendering tab and flip it while the page is open.

Try it yourself

  1. 1

    Read the tracks in both modes

    In "Choose the keyframes", press Show card and read the track: [opacity, transform], 240 ms, ease-out. Switch on Reduced motion, press again: [opacity], 120 ms.

  2. 2

    Set the reduced duration to zero

    Change reduce ? 120 : 240 to reduce ? 0 : 240. The second line prints opacity (0 ms): the card jumps to its end state with no fade, which hides the change.

Code & diagrams

Choose the keyframes

The script builds the animation both ways and prints each one's properties. Press Show card, then switch on the timeline's Reduced motion and press again: `opacity, transform` (240 ms) becomes `opacity` (120 ms).

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

reduce=false: opacity, transform (240 ms)
reduce=true: opacity (120 ms)
The setting changes while the page is open

A looping dot. The two buttons send the `change` event the browser sends when the device setting changes. The listener pauses and resumes the loop. The last line of the script puts your real setting back.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

setting turned on: loop is paused
setting turned off: loop is running

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the change listener

In "The setting changes while the page is open", delete the line mql.addEventListener("change", (event) => apply(event.matches)); and press Run.

terminal
$ Run, then read the console
── what you'll see ──
setting turned on: loop is running
setting turned off: loop is running
On a device with no preference the dot keeps looping after the setting is switched on.

Myth vs fact

Myth

My CSS handles reduced motion, so the page is covered.

Fact

CSS rules do not reach animations that JavaScript starts. Search your scripts for .animate(, requestAnimationFrame and library calls.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Centralise the question. A hundred scattered matchMedia calls drift apart. One module exposing motionAllowed() and onMotionChange() (Topic 7.7) is also the place where the site switch (Topic 7.5) plugs in.

Remember this

  1. 1

    The problem this solves: the reduce rules from Topic 7.2 only change CSS. Animations you start with element.animate(), requestAnimationFrame or a library are made by your code, so they ignore the setting unless your code asks. A page can pass every CSS check and still fly a card across the screen from a script.

  2. 2

    Reading the setting. matchMedia("(prefers-reduced-motion: reduce)") returns a MediaQueryList; its matches is true when the visitor asked for less motion. entrance() below reads it on each call and picks movement and a fade, or a short fade. Read it when the animation starts, not once at page load. A requestAnimationFrame count-up ignores CSS too: when motion is off, skip it and write the end value.

  3. 3

    When the setting changes while the page is open. MediaQueryList is an event target: addEventListener("change", (event) => …) gives event.matches. Use it to pause looping animations, as the third example does. A page cannot change the device setting, so the example sends the same MediaQueryListEvent the browser would. To see the real thing, emulate the media feature in the Chrome DevTools Rendering tab.

  4. 4

    Libraries have their own switches. GSAP's gsap.matchMedia() runs your setup only while a query is true and cleans up when it changes. Motion's <MotionConfig reducedMotion="user"> makes its components follow the device: per its types, it switches transform animations off and keeps opacity and colour. Phases 14 and 15 teach them.

  5. 5

    The server can know too. The Sec-CH-Prefers-Reduced-Motion client hint sends no-preference or reduce in a request header, so a server can render calm markup. MDN marks it experimental and not Baseline: an enhancement only.

Explain it without notes

01

Why does a CSS reduce block not stop an animation started with element.animate()?

02

How do you react when the visitor turns reduced motion on while the page is open?

Practice

01

Write the one expression that says whether the visitor prefers reduced motion.

02

Write fadeOrSlide(el) that slides an element in 16 px and fades normally, and only fades in 120 ms when the visitor prefers reduced motion.

Done when you can

  • You can read the setting with matchMedia and say what matches means

  • You choose calm keyframes or skip the animation in code you start yourself

  • You listen for change for anything that loops

  • You know the library switches and the client hint exist