Some people get physically ill from motion on a screen. Not annoyed. Ill. Dizzy or nauseous from a parallax hero or a big zoom, a real vestibular response that a well-meaning animation triggered without asking.
The web has a switch for those people. Honoring it is a few lines of CSS. Skipping it is a decision you are making about who gets to use the site comfortably, whether you meant to make that decision or not.
What prefers-reduced-motion is
prefers-reduced-motion is a media query that detects a user’s motion preference. The preference is set at the operating system level, not in the browser, which is the part people miss.
When someone turns on “Reduce motion” in macOS under Accessibility, or in Windows Settings, or on iOS and Android, the browser reports it. You read that report like any other media query.
@media (prefers-reduced-motion: reduce) {
/* styles for people who asked for less motion */
}
The query has two values. reduce means the user asked for minimal animation. no-preference means they did not, which is the default and what most visitors send. You almost always write for reduce and let everyone else fall through to your normal styles.
Support is not something to worry about. Every current browser has shipped this query since 2019, and any browser too old to understand it simply ignores the block and serves your full-motion version. That is a safe way to fail, because the people who need the reduced experience are on modern systems that support the query. You are not gambling with anyone’s comfort by relying on it.
The blunt global override and its cost
There is a famous snippet that nukes all motion in one shot. You have probably pasted it. It is a reasonable safety net and a poor final answer, and it is worth understanding both halves of that.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
It works by crushing every animation and transition down to almost nothing and forcing every loop to run exactly once. Look at the value: 0.01ms, not 0s. That is deliberate. Scripts that wait for the animationend or transitionend event still need that event to fire, and a true zero can skip it in some engines, leaving your JavaScript hung forever on a completion that never arrives. The tiny non-zero duration keeps the events firing while the motion stays invisible.
The cost is that it is indiscriminate. It kills motion that was doing a job, not just the decorative kind. A menu that slid open now snaps into place with no cue about where it came from. A spinner that was quietly saying “still working” freezes or disappears. And if any of your content only becomes visible through an animation, this override can leave it hidden, which is the exact trap the next section is about.
Keep the global override as a floor. It guarantees nothing on the site will ever assault someone who opted out. Then do the real work one component at a time.
Per-component reduced states
Design the calm version. Do not just delete the animation and hope, decide what each component looks like when it is standing still, and make that a real state.
The honest approach flips the default. Write your base styles with no motion at all, then add motion only inside a no-preference block. Motion becomes an enhancement that reduced-motion users never receive in the first place, instead of something you paste over the top and then claw back with !important.
.panel {
opacity: 1;
transform: none;
}
@media (prefers-reduced-motion: no-preference) {
.panel {
animation: slide-up 300ms ease-out;
}
}
Reduced-motion users get a panel that is simply present. Everyone else gets the slide. Nothing is hidden, nothing snaps, and you never fight your own override rules. This is more typing than the global nuke, and it is the version that actually respects the person.
When flipping the default would mean too much surgery on an existing site, target the specific component under reduce and swap its motion for something still. You can also pause a running animation rather than remove it, since animation-play-state can freeze a loop in place:
@media (prefers-reduced-motion: reduce) {
.marquee {
animation-play-state: paused;
}
}
Why hidden resting states need special care
What is the resting state of your element when the animation is not running? If the answer is opacity: 0 or width: 0, you have a problem, and the blunt override is what exposes it.
Reveal animations are usually built exactly this way. The element starts invisible or collapsed, and the animation is the only thing that brings it up to full opacity or full size.
.reveal {
opacity: 0;
animation: fade-in 400ms forwards;
}
@keyframes fade-in {
to { opacity: 1; }
}
Now run the global override over it. The duration collapses to 0.01ms and finishes instantly, and because of forwards it lands on opacity: 1, so this particular case survives by luck. Change one thing. Drop the forwards, or let some other rule win and leave the resting opacity: 0 in place, and the content is invisible for reduced-motion users. Permanently. They asked for less motion and you handed them a blank space where your paragraph should be.
The rule is short. The visible, finished state must be the resting state, not the animated one. Never lean on an animation to reveal content. Start from the on-screen values and animate outward from there, or make certain the fill-mode leaves the element visible even when the motion is crushed to zero. Then prove it: turn on Reduce motion, reload, and read the page. If something is missing, an animation was holding it hostage.
Fades vs movement for vestibular safety
Not all motion is equal, and reduced motion does not have to mean no motion. The thing that triggers vestibular disorders is movement across the screen: parallax, large translations, zooming, spinning. A fade in place moves nothing across the field of view, so it rarely bothers anyone.
So the good reduced-motion swap is usually not “remove the animation.” It is “replace the movement with a fade.” The element still appears, still feels responsive, but it cross-fades into view instead of flying in from the edge.
.toast {
animation: slide-in 250ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.toast {
animation: fade-in 200ms ease-out; /* same arrival, no travel */
}
}
Same feedback, no vestibular trigger. That is what taking the preference seriously looks like in practice: you keep the communication and drop the part that makes people sick.
A short list of what is most worth reducing, so you can spend your effort where it counts:
- Parallax, where layers scroll at different speeds.
- Anything that scales up past the edge of the viewport.
- Infinite background motion and auto-playing carousels.
- Large slides and spins that cross a lot of the screen.
A gentle opacity change or a small color shift is almost always fine to keep. Reduce the travel, not the feedback.
Reduced motion is not a corner case you bolt on at the very end. It is the same decision as performance, because the compositor-only fade you reach for here is also the cheap one from the performance guide. Headline effects like a gradient shine or a typewriter reveal need a reduced fallback too, usually the plain text with the effect skipped and the content fully readable. And every animation you write, from the parent CSS animation guide on down, should have a clear answer ready for the person who asked for less.