Shots in this scene
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.
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
matchMediareturns.matchessays whether the query is true now. It fires achangeevent 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
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
Set the reduced duration to zero
Change
reduce ? 120 : 240toreduce ? 0 : 240. The second line printsopacity (0 ms): the card jumps to its end state with no fade, which hides the change.
Code & diagrams
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).
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
reduce=false: opacity, transform (240 ms)
reduce=true: opacity (120 ms)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.
Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.
Console
Nothing logged yet.Expected console output
setting turned on: loop is paused
setting turned off: loop is runningBreak 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.
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
matchMediacalls drift apart. One module exposingmotionAllowed()andonMotionChange()(Topic 7.7) is also the place where the site switch (Topic 7.5) plugs in.
Remember this
- 1
The problem this solves: the
reducerules from Topic 7.2 only change CSS. Animations you start withelement.animate(),requestAnimationFrameor 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
Reading the setting.
matchMedia("(prefers-reduced-motion: reduce)")returns aMediaQueryList; itsmatchesistruewhen 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. ArequestAnimationFramecount-up ignores CSS too: when motion is off, skip it and write the end value. - 3
When the setting changes while the page is open.
MediaQueryListis an event target:addEventListener("change", (event) => …)givesevent.matches. Use it to pause looping animations, as the third example does. A page cannot change the device setting, so the example sends the sameMediaQueryListEventthe browser would. To see the real thing, emulate the media feature in the Chrome DevTools Rendering tab. - 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
The server can know too. The
Sec-CH-Prefers-Reduced-Motionclient hint sendsno-preferenceorreducein a request header, so a server can render calm markup. MDN marks it experimental and not Baseline: an enhancement only.
Explain it without notes
Why does a CSS reduce block not stop an animation started with element.animate()?
How do you react when the visitor turns reduced motion on while the page is open?
Practice
Write the one expression that says whether the visitor prefers reduced motion.
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
matchMediaand say whatmatchesmeansYou choose calm keyframes or skip the animation in code you start yourself
You listen for
changefor anything that loopsYou know the library switches and the client hint exist