简介
操作系统为此提供了一个补救措施:用户可以选择不使用动画。该设置主要是针对操作系统的,但网站和网络应用程序可以读取该值。它不会自动禁用动画,所以责任在于我们--开发者--要利用好它。
在本教程中,我们将看到如何使用prefers-reduced-motion
媒体查询来禁用React中的动画。
切入正题
已经熟悉了前庭系统和prefers-reduced-motion
媒体查询?
为什么有必要这样做?
人体是由许多不同的系统组成的,负责调节和管理整个生存的事情。其中一个系统被称为前庭系统。它包括内耳和大脑的一部分,它管理我们的平衡感。
你知道当你转得非常快时,会让你感到头晕吗?通过旋转,你在内耳中晃动液体,而大脑利用这些液体来帮助它找出哪个方向是 "向上"。这类似于水平仪的工作原理。
当你转圈的时候,你的大脑从不同的来源接收到不相容的信息:你的耳液声称是一件事,而你的眼睛却声称是另一件事。这种不和谐是超级混乱和不愉快的。
对大多数人来说,除非你故意要混淆这个系统,否则它可以正常工作,你可以通过你的生活,相信它将使你保持正直。但对其他人来说,这个系统并不总是值得信任的--前庭障碍导致前庭系统向大脑提供不良信息。
这方面最常见的症状是眩晕;突然间,有人会觉得重力把他们拉向错误的方向。这只是前庭失调的一种表现方式。
令人不安的是,动画可以成为一些人的诱因,导致一系列令人不快的副作用:头晕、恶心、头痛、乏力。它可以感觉到好像我们的网站正在伸出手来,让坐在办公椅上的人旋转😬。
据估计,在美国40岁以上的成年人中,有高达35%的人经历过某种形式的前庭功能障碍,有5%的人报告有慢性问题(来源)。
我们在开发动画时不应该忘记这些人。
选择不使用动画
几年来,操作系统一直让用户选择不使用动画,通常是在可访问性设置中。
令人高兴的是,现在所有的主流操作系统都有这个设置,包括桌面系统(MacOS 10.12以上,Windows 7以上,Linux)和移动系统(iOS,Android 9以上)。
当这个选项被勾选时,操作系统会禁用所有的动画(例如MacOS上最小化窗口时著名的精灵动画),但苹果决定开始使用媒体查询(prefers-reduced-motion
)向浏览器公开这一设置。这样一来,网站可以读取相同的值,并使用它来禁用动画。
除了操作系统层面的支持外,我们还需要考虑浏览器的支持。幸运的是,浏览器的支持是相当好的。
对于使用不支持该功能的浏览器或操作系统的人,我们将默认为无动画。这样,无论用户的设备和软件如何,我们都能从可访问性的角度得到保障。
使用媒体查询
动画可以在CSS中使用媒体查询来禁用。
.fancy-box {
width: 100px;
height: 100px;
transform: scale(1);
transition: transform 300ms;
}
.fancy-box:hover {
transform: scale(1.2);
}
@media (prefers-reduced-motion: reduce) {
.fancy-box {
transition: none;
}
}
在这种情况下,我们从动画被启用的地方开始,并根据媒体查询明确地禁用它们。一个更好的心理模型是反过来思考:开始时没有动画,如果用户希望的话可以启用。
.fancy-box {
width: 100px;
height: 100px;
transform: scale(1);
/* No more `transition` here! */
}
.fancy-box:hover {
transform: scale(1.2);
}
@media (prefers-reduced-motion: no-preference) {
.fancy-box {
transition: transform 300ms;
}
}
要明确的是,no-preference
是默认值。从来没有摆弄过他们的可访问性设置的用户仍然会看到我们的动画;从用户的角度来看,没有明确的 "选择 "要求。
通过改变它,使transition
是在媒体查询中设置的,我们确保对于不支持这个属性的浏览器/设备的用户来说,动画是默认禁用的。浏览器会忽略未被识别的媒体查询中的CSS,所以这个transition
对它们来说就像不存在一样。
有些人喜欢在他们的CSS文件中使用一个时髦的 "全局过渡取消设置"。比如说
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
这个方法的作用是将所有的动画减少到基本上是瞬时的,并使用!important
,以确保这个值覆盖其他所有的东西。选择0.01ms
,而不是0ms
,因为有些浏览器认为0毫秒是一个虚假的/空值。
如果你的动画完全由CSS驱动,这很好......但我在JS中运行动画时遇到了奇怪的问题。具体来说,我见过这种重置有相反的效果,让动画变得超级快,让人头晕目眩。
如果你打算在JS中运行一些动画(例如通过React Spring这样的库),我建议避免使用这种模式。
在JS领域
上面显示的媒体查询对于完全在CSS中发生的动画(如过渡、关键帧动画)非常有用。然而,有许多类型的动画不能完全通过CSS完成。
-
使用弹簧物理学的动画。
-
涉及光标坐标、滚动位置或其他 "环境 "因素的动画。
-
HTML5 Canvas动画。
-
某些类型的SVG动画。
本网站上的大多数动画都是在JS中进行的,因为它们符合这些类别中的一个(或多个)。
因为这个功能是以媒体查询的方式实现的,所以可以用我们在JS中访问任何媒体查询值的方式来访问它:使用window.matchMedia
。
function getPrefersReducedMotion() {
const QUERY = '(prefers-reduced-motion: no-preference)';
const mediaQueryList = window.matchMedia(QUERY);
const prefersReducedMotion = !mediaQueryList.matches;
return prefersReducedMotion;
}
mediaQueryList.matches
如果用户没有偏好--换句话说,他们没有勾选 "减少运动 "的复选框,则将 。记住,我们要检查的是 "无偏好"。如果他们勾选true
了该复选框,"无偏好 "将被 。因此,为了弄清用户是否喜欢减少运动,我们用 ,翻转这个布尔值。false
!mediaQueryList.matches
(我承认这有很多双负数--很抱歉!)。我们需要以这种迂回的方式来确保使用不支持的浏览器/设备的人禁用动画)。
我们还可以使用事件监听器来更新这个值。
const QUERY = '(prefers-reduced-motion: no-preference)';
const mediaQueryList = window.matchMedia(QUERY);
const listener = (event) => {
const getPrefersReducedMotion = getPrefersReducedMotion();
};
mediaQueryList.addEventListener('change', listener);
// Later:
mediaQueryList.removeEventListener('change', listener);
当用户在他们的操作系统中切换 "减少运动 "复选框时,这个监听器就会启动。
我们想监听这个事件,因为我们想在用户拨动该复选框时立即终止动画,即使页面已经加载/动画正在进行中。
钩子
我们可以用一个钩子将其与我们的React生命周期联系起来!
const QUERY = '(prefers-reduced-motion: no-preference)';
const getInitialState = () => !window.matchMedia(QUERY).matches;
function usePrefersReducedMotion() {
const [prefersReducedMotion, setPrefersReducedMotion] = React.useState(
getInitialState
);
React.useEffect(() => {
const mediaQueryList = window.matchMedia(QUERY);
const listener = (event) => {
setPrefersReducedMotion(!event.matches);
};
mediaQueryList.addEventListener('change', listener);
return () => {
mediaQueryList.removeEventListener('change', listener);
};
}, []);
return prefersReducedMotion;
}
这有很多代码,所以让我们把它分解一下。
-
我们通过检查媒体查询的初始值来初始化一些React状态
-
在挂载时,我们设置一个事件监听器,这样我们就可以在媒体查询发生变化时更新状态
-
在卸载时,我们删除事件监听器,以避免在组件大量卸载/重装时出现内存泄漏。
-
我们只想在组件挂载时运行这个效果,所以我们传递一个空的依赖性数组。
-
我们返回一个布尔值,代表用户是否禁用了动画。
SSR安全
如果你试图在Gatsby或Next.js应用程序中按原样使用这个钩子,你会得到一个错误:'window' is not defined
。
Gatsby和Next利用了服务器端渲染的优势,这意味着HTML在被发送到浏览器之前,已经在某个点上进行了预渲染。当我们第一次渲染我们的React组件树时,我们不知道用户是否喜欢减少动作
事实证明,这是一个棘手的问题;我在我的博文The Perils of Rehydration中进行了深入探讨。TL:DR;就是我们在客户端的第一次渲染需要与服务器上的原始渲染相匹配。
这里有一个更新的钩子,对SSR是安全的。
const QUERY = '(prefers-reduced-motion: no-preference)';
function usePrefersReducedMotion() {
// Default to no-animations, since we don't know what the
// user's preference is on the server.
const [
prefersReducedMotion,
setPrefersReducedMotion
] = React.useState(true);
React.useEffect(() => {
const mediaQueryList = window.matchMedia(QUERY);
// Set the true initial value, now that we're on the client:
setPrefersReducedMotion(
!window.matchMedia(QUERY).matches
)
// Register our event listener
const listener = (event) => {
setPrefersReducedMotion(!event.matches);
};
mediaQueryList.addEventListener('change', listener);
return () => {
mediaQueryList.removeEventListener('change', listener);
};
}, []);
return prefersReducedMotion;
}
为了保证SSR的安全,我们必须做出妥协:我们将在第一次渲染时禁用所有用户的动画。如果你有一些动画在加载时立即运行,它们可能会因为这个钩子而对所有人都失效。出于这个原因,我建议如果你使用Gatsby/Next或其他任何SSR实现,只使用这个钩子的变体。
在行动中
下面是我们如何使用这个钩子与React Spring,一个基于弹簧物理学的动画库。
import { useSpring, animated } from 'react-spring';
const Box = ({ isBig }) => {
const prefersReducedMotion = usePrefersReducedMotion();
const styles = useSpring({
width: 100,
height: 100,
background: 'rebeccapurple',
transform: isBig ? 'scale(2)' : 'scale(1)',
immediate: prefersReducedMotion,
});
return <animated.div style={styles}>Box!</animated.div>;
};
React Spring让我们通过将immediate
设置为true
来禁用运动,所以我们可以将我们的prefersReducedMotion
布尔值直接传递给它
这个钩子抽象的好处是,它是即插即用的;你可以把它放到任何组件中,只用两行代码就可以把它连接起来。最重要的是,它是反应性的--它能立即适应偏好的变化。
没有钩子
这个自定义钩子非常适合由JS驱动的动画,比如那些使用React Spring的动画。对于纯粹的CSS动画,你可以使用媒体查询!
例如,这里有一个使用我们的新钩子的动画,和内联样式,有styled-components
。
const Box = ({ isBig }) => {
const prefersReducedMotion = usePrefersReducedMotion();
const styles = {
transform: isBig ? 'scale(2)' : 'scale(1)',
transition: prefersReducedMotion ? undefined : 'transform 300ms',
};
return <Wrapper style={styles}>Box!</Wrapper>;
};
const Wrapper = styled.div`
width: 100px;
height: 100px;
background: rebeccapurple;
`;
在这种情况下,你可以使用钩子,但因为我们谈论的是一个CSS过渡,所以最好使用 "vanilla "媒体查询。
const Box = ({ isBig }) => {
const styles = {
transform: isBig ? 'scale(2)' : 'scale(1)',
};
return <Wrapper style={styles}>Box!</Wrapper>;
};
const Wrapper = styled.div`
width: 100px;
height: 100px;
background: rebeccapurple;
@media (prefers-reduced-motion: no-preference) {
transition: transform 300ms;
}
`;
usePrefersReducedMotion
钩子是一把锤子,但不是每个动画都是钉子。这两种解决方案都能很好地工作,但后一种只用CSS的方法更直接,更容易操作。
顺便说一下,你可能想知道为什么我们要走这个兔子洞,而不是简单地提供一个你可以安装和使用的NPM包。原因是,了解原因是很好的。你现在有了一个可以用来处理各种情况的心理工具集,而不是一个把每个动画都当成钉子的僵硬的包。
结语
作为人类,我们倾向于围绕自己的经验进行偏向。黄金法则"--以其人之道还治其人之身--完全忽视了人与人之间是不同的这一事实!
不是每个人都以同样的方式体验事物,我们需要注意这一点。一个让我高兴的动画可能会让其他人感到头晕目眩,以至于之后需要躺上半个小时。
每一个被添加到浏览器中的非微不足道的功能都是大量艰苦工作和协调的结果。prefers-reduced-motion
媒体查询存在于每一个主要的浏览器中,这一事实证明了浏览器供应商投入了大量的工作,更不用说那些首先建立了控件的操作系统开发者了!他们给了我们一些工具,让我们能够在浏览器上进行操作。