DEV Community

lichongyang130
lichongyang130

Posted on

Your animations are fine. Your prefers-reduced-motion handling probably isn't.

Your animations are fine. Your prefers-reduced-motion handling probably isn't.

Here's a trap I fell into for years: I did add a reduced-motion block. It was right there at the bottom of my global CSS, three lines long, and I felt good about it. It looked like this:

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}
Enter fullscreen mode Exit fullscreen mode

This passes an automated a11y audit. It also breaks your product for the exact people it was written for.

Why the nuclear option backfires

animation: none doesn't "reduce" anything. It deletes the motion — including the motion that carries meaning. Kill that and you get:

  • Drawers and modals that teleport. A slide-in panel that used to take 240ms now appears instantly in its final position. There's no longer any spatial relationship between where it was and where it went.
  • Focus loss on scroll-triggered content. A section that faded in on scroll now just doesn't render its transition state — and depending on how you built it, sometimes doesn't render at all.
  • Skeletons that lie. A pulsing loading skeleton frozen mid-opacity can read as "finished, empty" instead of "still loading."

Reduced motion is not "no motion." It's less vestibular-triggering motion. The WCAG target is animation that doesn't involve large-scale movement, parallax, or scaling — not the absence of all feedback.

What I do instead

The rule I've landed on: replace, don't remove. Four patterns cover almost everything.

1. Swap a transform for a cross-fade. Movement is the trigger, opacity isn't. So a slide becomes a fade at the same duration.

.panel {
  transition: transform 240ms ease, opacity 240ms ease;
}
@media (prefers-reduced-motion: reduce) {
  .panel { transform: none !important; transition: opacity 240ms linear; }
}
Enter fullscreen mode Exit fullscreen mode

2. Keep the easing, cap the travel. A 16° tilt or a 400px parallax is the problem. 2° of tilt still reads as "interactive" and triggers nothing.

3. Animate a non-spatial property. Progress is the useful half of a loading animation. Drive it with width, a background gradient position, or a text counter instead of a spinning element.

4. Keep state changes, shorten them. A 40ms opacity flip is not a vestibular trigger. It's still enough to tell the user "this changed because of you."

And the part people forget: reduced motion is also about scroll and pointer. Smooth-scroll libraries, scroll-jacking, magnetic cursor effects, and auto-playing background loops all need the same treatment as your CSS transitions. Your <html> scroll-behavior counts.

The testing gap

Here's the honest part: most of this passes automated checks. A script can verify that a reduced-motion media query exists. It cannot tell you whether your modal now teleports.

The only way I've found to actually catch it:

  1. Toggle the OS setting for real — macOS Reduce motion, Windows Show animations off, or Firefox's ui.prefersReducedMotion flag if you want to avoid changing your desktop.
  2. Walk your five most important flows with it on. Open the modal. Submit the form. Scroll the pricing page.
  3. Ask one question per interaction: can I still tell what happened?

If the answer is no, you didn't reduce the motion. You removed the information.

Receipts over vibes

The reason I care about this beyond principle: motion components are the ones people paste in blind. You grab a tilt card or an aurora background, it looks great, and nobody checks what it does under reduced-motion until an audit or a complaint.

That's a fixable information problem. Every animated asset on Motif UI ships with an accessibility score next to its bundle size and dependency count — the library is nothing but original components, all MIT, and the a11y number is part of the audit gate an item has to clear before it gets a card at all. Tilt Card is 2.9 KB with zero deps; Aurora Veil is pure CSS and runs at 60fps with 0 JS. Those numbers are on the page, which is the point — you can check them instead of trusting a screenshot.

Six lines of CSS is not accessibility. Knowing what those six lines do to your users is.

Top comments (0)