FIELD NOTE 02 / 03

2026.08.02 · 本期

把动效做成呼吸,而不是烟花

动效不是给页面加戏,而是把因果变得可见。用时长、缓动与降级策略,建立一套克制而可靠的界面节奏。

深色背景上有多条交错的米色波浪线与一枚朱红圆点,下方写着「把动效做成呼吸」。
封面插画 / FOLD Press

WORDS BY

Caz 设计与代码的慢笔记
发布
阅读
约 4 分钟
编号
NO. 002

FILED UNDER

返回文章列表

我第一次认真对待动效,是在一个加载按钮上。它花了 600 毫秒旋转,又用另一段 600 毫秒淡出,最后界面才告诉用户「提交成功」。从视觉上看,它挺优雅;从体验上说,它让每一次点击都多等了一秒多。用户不需要看一场关于等待的表演,用户只是想知道事情有没有发生。

后来我把那段动画删到 160 毫秒。点击之后,按钮变为完成态,文案从「提交」安静地换成「已发送」。没有人称赞过这个改动,但也没有人再问「我到底点上了没有」。

这大概是动效最理想的状态:不是被看见,而是被感受到。

先回答因果,再讨论风格

动效的第一职责不是装饰,而是解释变化。

一个面板从右侧滑入,意味着它从右侧来,也可以向右回去。一个对话框从中心放大,意味着它占据了当前焦点的中心。一个列表项在删除时收起,意味着它不再占据空间。反过来,如果所有东西都用同一种淡入淡出,观众就失去了理解空间关系的机会。

所以在写 transition 之前,我会先问三个问题:

  • 什么发生了变化?
  • 变化从何处开始,去往何处?
  • 用户需要多快知道结果?

这三个答案会直接决定动画的时长、方向和缓动。风格是最后一步,不是第一步。

三种节奏,而不是一个时长

界面里的动效大致可以分成三层。它们的目的不同,时间也应该不同。

  1. 反馈(100–160ms):按钮按下、开关切换、链接获得焦点。它必须几乎即时,让动作和结果像同一件事。
  2. 过渡(180–280ms):抽屉展开、卡片翻面、内容切换。它需要足够长,让人看见空间关系,但足够短,不打断下一步。
  3. 叙事(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 下,信息和状态是否仍然完整?
  • 在低端设备和慢网络上,第一帧是否仍然很快出现?

好的动效像呼吸:你注意不到它正在发生,但一旦停止,就会立刻觉得哪里不对。它不是烟花,不需要在每一次点击后绽放。它只需要在对的时刻,给出一个可信的回应。