Guides · 6 min read · Updated
How do you support prefers-reduced-motion without hurting animation performance?
Treat reduced motion as a second design of the same page: in CSS and JavaScript, replace movement with a still state or a gentle fade, and stop loops that no longer need to run. That also makes the page faster, because the cheapest animation is the one that is not running, and the same habits (transform and opacity, pausing offscreen) serve both goals.
What the Animated Web Report found
The Animated Web Report 2026 scanned the code of 429 live animated demos in the AnimatedWebs catalog. 99.8% of them (428) contain a prefers-reduced-motion rule, which is the baseline. Only 61.5% (264) also include a pause control for motion, and 59.9% have a breakpoint at 760px or narrower.
On weight, the median demo folder is 718 KB without video, and one in ten is larger than 5.2 MB. Video accounts for 44.1% of the bytes in the 91 demos that ship it. WebGL demos have a median of 715 KB, canvas demos 4.1 MB and CSS-only demos 71 KB.
These are static text scans of one catalog, not the whole web, and the report says so. A page that mentions prefers-reduced-motion may handle it in only one section. Read the figures as a picture of how a careful set of animated pages is built, then test your own page. The gap between the two headline shares is the useful part: the rule is nearly universal, and the pause control trails well behind it.
| Measure | Share of demos | Count |
|---|---|---|
| Contains a prefers-reduced-motion rule | 99.8% | 428 of 429 |
| Also has a pause control | 61.5% | 264 of 429 |
| Has a breakpoint at 760px or narrower | 59.9% | 257 of 429 |
| Lazy loads (loading attribute or IntersectionObserver) | 66.4% | 285 of 429 |
| Uses an IntersectionObserver | 63.9% | 274 of 429 |
The full method and data are on the report page.
What reduced motion actually asks for
The prefers-reduced-motion media query has two values, no-preference and reduce. A visitor who turned on a reduced-motion setting in their operating system or browser matches reduce. MDN describes it as a request to remove, reduce or replace non-essential motion, and notes that scaling or panning large objects can cause discomfort for people with vestibular motion disorders.
The word to notice is replace. Reduced motion does not mean no feedback and it does not mean a broken page. A fade, a color change or a still frame still tells the visitor that something happened. What you remove is travel across the screen, large scaling, parallax and anything that loops.
| Effect | With reduced motion |
|---|---|
| Slide and fade reveals | Show the final state, or fade only |
| Parallax and scroll-driven camera | Fixed position, no scroll linking |
| Looping video background | A still poster, with a play control |
| WebGL animation loop | Render one still frame |
| Auto-scrolling marquee | A static row, or a manual scroller |
| Hover and focus feedback | Keep it: color and opacity are fine |
The CSS side
Write the animated version as the default and override it inside the media query, or the reverse, with the animation only added when the visitor has no preference. The second is safer, because a browser that does not understand the query gets the still page.
/* Calm by default */
.card { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
.card { animation: rise 700ms cubic-bezier(.2,.7,.2,1) both; }
@keyframes rise { from { opacity: 0; transform: translateY(24px); } }
.hero-video { display: block; }
}
/* Replace, do not just remove */
@media (prefers-reduced-motion: reduce) {
.card { animation: fade 300ms linear both; }
@keyframes fade { from { opacity: 0; } }
.hero-video { display: none; }
.hero-poster { display: block; }
}Avoid the blunt rule that sets every animation and transition duration to zero. It also breaks things that rely on an animation ending, such as a panel that waits for its transition before it is removed. Be specific about what you replace.
The JavaScript side
Scripts need the same decision, and it can change while the page is open, because a visitor can toggle the setting. Read it with matchMedia and listen for changes so your loops start and stop with it.
const query = matchMedia("(prefers-reduced-motion: reduce)");
let loop = null;
function start() {
if (loop !== null) return;
const tick = (now) => { draw(now); loop = requestAnimationFrame(tick); };
loop = requestAnimationFrame(tick);
}
function stop() {
if (loop !== null) cancelAnimationFrame(loop);
loop = null;
}
function apply() {
if (query.matches) { stop(); draw(0); } // one still frame
else start();
}
query.addEventListener("change", apply);
apply();
function draw(now) { /* your drawing code */ }The draw(0) call matters. A canvas that stops with no frame is blank, which is worse than animated. Always leave the still state on screen.
Add a pause control
Reduced motion helps visitors who set it. Others still benefit from a way to stop motion on the page. The Web Content Accessibility Guidelines require a mechanism to pause, stop or hide moving content that starts automatically and lasts more than five seconds, at the baseline Level A. In our scan only 61.5% of demos have one, so it is both a gap and an easy win.
<button id="motion-toggle" aria-pressed="false">Pause motion</button>
<script>
const btn = document.getElementById("motion-toggle");
btn.addEventListener("click", () => {
const paused = document.documentElement.toggleAttribute("data-paused");
btn.setAttribute("aria-pressed", String(paused));
btn.textContent = paused ? "Play motion" : "Pause motion";
// also call your own stop() or start() here for canvas loops
});
</script>
<style>
[data-paused] *, [data-paused] *::before, [data-paused] *::after {
animation-play-state: paused !important;
}
</style>That pauses CSS animations. A canvas or WebGL loop needs its own stop call, which is the comment in the script. Put the button where keyboard users reach it early.
The performance side: the same habits
Supporting reduced motion and keeping a page fast overlap more than they seem to. Both push you toward doing less work. The habits below serve both.
- Animate transform and opacity. web.dev advises avoiding properties that trigger layout or paint unless they are necessary.
- Stop work nobody can see. 63.9% of the demos we scanned use an IntersectionObserver, which is how a loop learns it left the screen.
- Pause when the tab is hidden. The browser already pauses requestAnimationFrame in background tabs, but timers and videos may need your help.
- Use will-change sparingly. web.dev says to apply it only to elements that are about to change, and not early in optimization.
- Budget your bytes. The median demo is 718 KB without video, so a page far above 5.2 MB sits in the heaviest tenth.
- Load heavy scenes late. A poster first, the scene when it nears the viewport.
For the full phone checklist, see the phone performance guide. For the render loop details, see the three.js guide.
How to test it
A scan can only tell that the rule exists, so test the behavior.
- 1Turn on the reduced-motion setting in your operating system (on Windows, turn off animation effects in Accessibility settings) and reload the page.
- 2Check that nothing travels, loops or autoplays, and that no area is blank.
- 3Toggle the setting while the page is open. Loops should stop and start without a reload.
- 4Press Tab from the top of the page. The pause control should be reachable early and clearly labeled.
- 5Open the browser's performance panel with reduced motion on, and confirm the main thread goes quiet.
Fold these checks into your release routine. AnimatedWebs templates are built to a quality bar that includes a calm reduced-motion state, and you can play the live demos and test them the same way before you start.
Video and other media
Autoplaying video is the largest single source of both motion and weight. Give a looping background clip a poster image, keep the clip short, and let reduced motion swap the clip for the poster. A visitor who asked for less movement should not have to dismiss a video to read your headline.
Animated images follow the same rule. A GIF or animated WebP cannot be paused from CSS, so serve a still version under reduced motion with the picture element and a media query, or control the animation from a script.
Mistakes that show up again and again
- Handling the query in one section only. A rule in the hero does nothing for the footer marquee.
- Removing the animation and leaving the content hidden. If the start state is invisible, the end state has to apply too.
- Stopping a canvas loop without drawing a frame, which leaves a blank box.
- Checking once. A visitor can change the setting while the page is open, so listen for the change event.
- Treating a pause control as a replacement. It is an addition, and some visitors will never find it.
Every one of these passes a text scan for the media query and still fails a visitor, which is why the testing steps above matter more than the count of pages that mention the rule.





