What the setting asks for
Operating systems let people ask for less motion, and browsers pass that on as the media query prefers-reduced-motion. For people with vestibular disorders, large movement tied to scrolling can bring on dizziness or nausea. WCAG 2.1 names it in success criterion 2.3.3, Animation from Interactions: motion that an interaction sets off can be turned off, unless it is essential.
/* CSS: the same media query the scripts read. */
@media (prefers-reduced-motion: reduce) {
.reveal { opacity: 1; transform: none; transition: none; }
}
// JavaScript
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
if (reduce.matches) {
// show the finished state now
}The value is reduce, and that is the brief: take out motion that plays at the reader, and keep everything they came for. Below, effect by effect.
| Effect | Under reduced motion | Why |
|---|---|---|
| A path drawing itself, a fade-up, split text, a counter, parallax | Show the finished state at once | Decoration played at the reader |
| A video scrubbed by the scroll | Show the end frame | The frames move on their own |
| Snapping to the next section | Jump instead of glide | It moves the page itself |
| A horizontal strip moved by the scroll | Keep it moving with the scroll | Follows the reader’s own input; holding it hides the panels |
| A looping animation on a timer | Stop it | Motion with no input at all |
Reveals and drawings: show them finished
A path drawing itself, a card fading up, a headline arriving word by word, a counter running to its number, a parallax layer: all decoration, played at the reader. Under reduced motion, show the finished state the moment the page loads, and run whatever was waiting for the animation to end.
function reveal(el) {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
el.style.opacity = '1';
el.style.transform = 'none';
onRevealed(el); // whatever waits for the reveal still runs
return;
}
// ...otherwise animate from the hidden state as the element scrolls in
}Snapping: jump instead of glide
A snap that eases the page to the next section moves the whole viewport by itself. That is exactly the motion the setting is about. Keep the snap and drop the glide: go there in one step.
function snapTo(top) {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
window.scrollTo({ top, behavior: reduce ? 'instant' : 'smooth' });
}Scrubbing that follows the scroll can stay
Some effects move only while the reader scrolls, one pixel for one pixel: a horizontal strip of panels, a drawing scrubbed back and forth. That is closer to dragging something than to watching it, and freezing it has a cost. A horizontal strip shows its panels only through that movement; hold it still and every panel but the first is out of reach, for exactly the readers who asked for less motion.
So this is a judgement, not a rule: keep scrubbing that maps directly to the reader’s own scroll, and make it easy to opt out where the page has another way to the same content. A loop that plays on a timer is different. It moves with no input at all, and it should stop.
Read it live, not once
The setting can change while the page is open. Reading matchMedia once at start-up misses that until the next load. Listen for the change, and apply it to anything still to come.
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
let reduced = reduce.matches;
reduce.addEventListener('change', (e) => {
reduced = e.matches; // a setting changed mid-visit applies now, not on reload
});The two directions are not equal. Turned on, finish whatever is still moving, straight away: the reader may have turned it on because the page was making them unwell. Turned off, leave what is already shown alone and let motion come back on the next load; hiding content again so it can animate in is worse than one missing animation.
Test it in a real browser
Playwright emulates the setting. Set it before the page loads, because most scripts read it once when they start, then check the finished state.
import { test, expect } from '@playwright/test';
test('reduced motion shows the cards finished', async ({ page }) => {
// Before the page loads: the scripts read the setting when they start.
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('/');
await expect(page.locator('.card').first()).toHaveCSS('opacity', '1');
});In Chrome’s DevTools, the Rendering panel has the same switch (Emulate CSS media feature prefers-reduced-motion) for checking by eye.
What svg-scroll-draw does
Every effect in the library follows the table above. Drawings, reveals, split text, counters and parallax show their finished state and still fire onComplete; a scrubbed video shows its end frame; scrollSnap jumps instead of animating; scrollHorizontal keeps scrubbing by default, with respectReducedMotion: true to hold it; a timeline keeps its scrubbing and stops its timed loop. Each behaviour is pinned by a test that emulates reduced motion: browser tests in Chromium, Firefox and WebKit for the reveals, text, counters, parallax, video, snapping, the horizontal strip and the timeline, and unit tests for the path drawing at start-up.
Since 2.22.0 every effect also follows the setting live (2.23.0 for the AOS, ScrollReveal and vivus drop-ins), the way the section above describes. Turned on mid-visit, each one finishes at once, exactly as a page loaded under reduced motion shows it, and fires onComplete once if it had not finished. Turned off again, what finished stays finished until the next load; snapping and the timeline’s loop, which hide nothing either way, follow it in both directions. Browser tests turn the setting on after the page has loaded, mid-drawing, mid-video and mid-story, in all three engines.
import { scrollReveal } from 'svg-scroll-draw/reveal';
import { scrollSnap } from 'svg-scroll-draw/snap';
import { scrollHorizontal } from 'svg-scroll-draw/horizontal';
scrollReveal('.card'); // reduced motion: every card shown, no transition
scrollSnap('.section'); // reduced motion: jumps instead of gliding
scrollHorizontal('.track'); // keeps scrubbing; { respectReducedMotion: true } holds it
// Turned on mid-visit, each one finishes at once (2.22.0+).