炫酷动画在有些人眼里是噩梦,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,直接用就行。很多大厂的首页其实已经悄悄加上了这个处理,只是大多数前端团队还没意识到自己的产品在这个细节上是缺失的。
下一步:打开你的浏览器的「辅助功能」设置,把减少动画打开,然后打开你负责的项目首页看一眼。
评论区
登录后可评论。