炫酷动画在有些人眼里是噩梦,WCAG 2.1 早就给答案了——prefers-reduced-motion 完整落地指南

以前做动画页面,客户说「有的用户会头晕」,我第一反应是「那是他们的问题」。后来才知道 WCAG 2.1 早就把这个写进了标准,而且 macOS、Windows、iOS 都有用户主动打开这个选项——你那个炫酷的入场动画,在这些用户眼里可能是灾难。

prefers-reduced-motion 这个媒体查询,就是给这个场景准备的。

它能做什么

当用户在系统设置里打开「减少动画」或「减弱动画效果」时,prefers-reduced-motion 的值会变成 reduce。在这个状态下,你应该把页面里所有非必要的动画都关掉——不是减小幅度,是直接拿掉。

/* 普通用户的动画 */
.card {
  animation: slideIn 0.4s ease-out;
}

/* 减少了动画偏好的用户:动画全部拿掉 */
@media (prefers-reduced-motion: reduce) {
  .card {
    animation: none;
    transition-duration: 0.01ms;
  }
}

注意这里 transition-duration 设成 0.01ms 而不是 0。有些浏览器对 0 的过渡会有特殊处理,设成极小值能保证浏览器知道你明确想要一个即时过渡。

完整的最佳实践

光关掉 animation 不够,滚动动画、视差效果、loading 状态都得处理:

/* 全局减少动画策略 */
@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;
  }
}

对于视差这种没法用 animation-duration 关的,得直接不用这个效果:

.hero {
  transform: translateY(20px); /* 视差偏移 */
}

@media (prefers-reduced-motion: reduce) {
  .hero {
    transform: none; /* 直接不要视差 */
  }
}

在 JavaScript 里怎么用

有时候动画逻辑是在 JS 里控制的,这时候也得判断:

const prefersReducedMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;

if (prefersReducedMotion) {
  // 跳过动画,直接执行最终状态
  element.classList.add("no-animation");
} else {
  // 正常跑动画
  element.classList.add("animate-slide-in");
}

window.matchMedia 返回的是一个 MediaQueryList 对象,支持监听变化——用户在使用过程中切换了系统设置,页面也得跟着变:

const mediaQuery = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
);

mediaQuery.addEventListener("change", (e) => {
  if (e.matches) {
    document.body.classList.add("reduced-motion");
  } else {
    document.body.classList.remove("reduced-motion");
  }
});

容易忽略的几个地方

滚动驱动的动画Intersection Observer 配合动画的场景,在 reduced motion 下 Observer 本身还在工作,但触发的动画不该跑:

@media (prefers-reduced-motion: reduce) {
  .fade-in-on-scroll {
    opacity: 1; /* 直接显示,不要淡入动画 */
  }
}

视频和 GIF:这个媒体查询管不了 <video> 和 GIF,得自己处理:

@media (prefers-reduced-motion: reduce) {
  video,
  gif {
    /* 静态图替代,或者播放 controls 让用户自己决定 */
    opacity: 0.7;
  }
}

第三方组件:你用的轮播库、动画组件,得自己手动加覆盖规则,组件本身不会自动响应这个媒体查询。

怎么在项目里落地

第一步:先在全局加这条覆盖规则,保证页面不会因为第三方组件出现不适的动画。

第二步:项目动画相关的 CSS 统一走一个动画基类,在 reduced motion 下统一关闭:

/* 动画基类 */
.animate-fade-in,
.animate-slide-up,
.animate-scale {
  animation-fill-mode: both;
}

@media (prefers-reduced-motion: reduce) {
  .animate-fade-in,
  .animate-slide-up,
  .animate-scale {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

第三步:在 code review 里加一条:新增动画必须同时处理 reduced motion 场景。


这个媒体查询在 caniuse 上全球支持率已经超过 97%,不需要 polyfill,直接用就行。很多大厂的首页其实已经悄悄加上了这个处理,只是大多数前端团队还没意识到自己的产品在这个细节上是缺失的。

下一步:打开你的浏览器的「辅助功能」设置,把减少动画打开,然后打开你负责的项目首页看一眼。

评论区

0 条评论

登录后可评论。

小鹿·界面实验室 1484 阅读