我第一次认真对待动效,是在一个加载按钮上。它花了 600 毫秒旋转,又用另一段 600 毫秒淡出,最后界面才告诉用户「提交成功」。从视觉上看,它挺优雅;从体验上说,它让每一次点击都多等了一秒多。用户不需要看一场关于等待的表演,用户只是想知道事情有没有发生。
后来我把那段动画删到 160 毫秒。点击之后,按钮变为完成态,文案从「提交」安静地换成「已发送」。没有人称赞过这个改动,但也没有人再问「我到底点上了没有」。
这大概是动效最理想的状态:不是被看见,而是被感受到。
先回答因果,再讨论风格
动效的第一职责不是装饰,而是解释变化。
一个面板从右侧滑入,意味着它从右侧来,也可以向右回去。一个对话框从中心放大,意味着它占据了当前焦点的中心。一个列表项在删除时收起,意味着它不再占据空间。反过来,如果所有东西都用同一种淡入淡出,观众就失去了理解空间关系的机会。
所以在写 transition 之前,我会先问三个问题:
- 什么发生了变化?
- 变化从何处开始,去往何处?
- 用户需要多快知道结果?
这三个答案会直接决定动画的时长、方向和缓动。风格是最后一步,不是第一步。
三种节奏,而不是一个时长
界面里的动效大致可以分成三层。它们的目的不同,时间也应该不同。
- 反馈(100–160ms):按钮按下、开关切换、链接获得焦点。它必须几乎即时,让动作和结果像同一件事。
- 过渡(180–280ms):抽屉展开、卡片翻面、内容切换。它需要足够长,让人看见空间关系,但足够短,不打断下一步。
- 叙事(400–600ms):首次进入页面、完成一个重要流程、空状态被填充。这类动效很少出现,一旦出现,可以稍微慢一点。
最常见的错误,是把「叙事」的时长用在「反馈」上。按钮转圈 500 毫秒,菜单展开 700 毫秒,悬停阴影变化 400 毫秒——每个单独看都不算灾难,叠在一起就成了一场排队。
时长之外是缓动。自然界的运动很少是匀速的。一个元素从静止出发,需要加速;接近终点时,需要减速。cubic-bezier(0.22, 1, 0.36, 1) 这样的曲线能让位移显得轻,而 ease-in-out 更适合对称的往返。线性运动只应该留给真正持续的东西,比如进度条和无限轮播。
:root {
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in-out-soft: cubic-bezier(0.65, 0, 0.35, 1);
--duration-feedback: 140ms;
--duration-transition: 240ms;
}
.card {
transition:
transform var(--duration-transition) var(--ease-out-soft),
box-shadow var(--duration-transition) var(--ease-out-soft);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 18px 40px rgb(20 18 14 / 0.12);
}
.card:active {
transition-duration: var(--duration-feedback);
transform: translateY(-1px);
}
这段代码里最重要的不是 translateY(-4px),而是悬停和按下的节奏不同。前者是邀请,允许慢一点;后者是确认,必须马上发生。
尽量只动 transform 和 opacity
浏览器可以非常便宜地合成 transform 与 opacity 的变化;而 width、height、top、left 往往会触发布局,box-shadow 和滤镜也可能造成昂贵的绘制。设计稿里一个漂亮的展开效果,如果实现为高度动画,很可能在低端手机上变成卡顿。
需要展开与收起时,优先使用裁剪与变换:
.panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 240ms var(--ease-out-soft);
}
.panel[data-open="true"] {
grid-template-rows: 1fr;
}
.panel > div {
overflow: hidden;
}
grid-template-rows 仍然会触发布局,但它的表现通常比逐个测量高度稳定,也不依赖 JavaScript。真正对性能敏感的场景,再考虑 FLIP 或 View Transitions API。
不过要提醒自己:性能不是只看属性表。一个 80 毫秒的布局动画,可能比一个 600 毫秒的变换动画更不引人注意。以用户感知到的顺畅为准,而不是以 CSS 属性是否在「安全名单」里为准。
尊重「不想动」的人
prefers-reduced-motion 不是可选项。
@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;
}
}
这段常见的「核弹式」降级很有效,但它也删掉了一些必要的状态反馈。更好的做法是先把动效分层:装饰性的位移和缩放可以完全关闭,状态颜色、焦点轮廓和加载指示仍然保留。对于前庭功能敏感的用户,位移、视差和大面积缩放比淡入淡出更容易引起不适;减少这些,比把所有东西都冻结更体贴。
动效设计的一部分,就是接受「不是每个人都会看到同一个版本」。这句话不只适用于减弱动态效果,也适用于慢网络、低性能设备和被放大的文字。
动效应该留下什么
我现在用一张很短的清单检查动效:
- 去掉这段动画,用户是否仍然知道发生了什么?如果知道,它可能只是装饰。
- 它是否帮助用户理解方向、层级或因果关系?
- 动画期间,用户能否继续操作?不能的话,它是否真的必要?
- 在
prefers-reduced-motion: reduce下,信息和状态是否仍然完整? - 在低端设备和慢网络上,第一帧是否仍然很快出现?
好的动效像呼吸:你注意不到它正在发生,但一旦停止,就会立刻觉得哪里不对。它不是烟花,不需要在每一次点击后绽放。它只需要在对的时刻,给出一个可信的回应。