写动效最容易翻车的不是时长,是曲线。同一个 200ms 的位移,用 linear 像 PPT,用 ease 像浏览器,用 emphasized 像 Material App,用 spring 像 iOS。曲线选对了,动效”档次”就对了。
CSS 默认值不是 linear
.box { transition: transform 0.3s; }
省略曲线时浏览器用 ease,等价于 cubic-bezier(0.25, 0.1, 0.25, 1)。这条曲线 1996 年由 Internet Explorer 引入,后被 CSS 标准沿用——今天它是”网页味”的代名词。
要做出 App 感,得跳出这条默认值。
五条标准曲线
CSS 内置五个关键字,对应五条 cubic-bezier:
| 关键字 | x1, y1, x2, y2 | 直觉 |
|---|---|---|
linear | 0, 0, 1, 1 | 匀速,像机械 |
ease | 0.25, 0.1, 0.25, 1 | 浏览器默认,老气 |
ease-in | 0.42, 0, 1, 1 | 加速冲出 |
ease-out | 0, 0, 0.58, 1 | 减速落地 |
ease-in-out | 0.42, 0, 0.58, 1 | 两头慢中间快 |
这五条是基线但不够用。现代 App 普遍用更激进的曲线表达”敏捷感”。
进入用 ease-out,退出用 ease-in
物理直觉:物体落地是减速,物体起飞是加速。映射到 UI:
/* 对话框淡入 + 上移 —— 减速到位 */
.modal-enter {
animation: modalIn .25s cubic-bezier(0, 0, 0.2, 1) both;
}
@keyframes modalIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
/* 对话框淡出 + 下移 —— 加速离开 */
.modal-exit {
animation: modalOut .2s cubic-bezier(0.4, 0, 1, 1) both;
}
@keyframes modalOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(16px); }
}
注意进入 250ms、退出 200ms——退出永远比进入快 20-30%,这是 Material / Apple 共识。用户对”消失”的等待容忍度低于”出现”。
Material 3 四条曲线
Google 把曲线分成 standard / emphasized × decelerate / accelerate 四组:
:root {
/* 主导动效:页面切换、抽屉、Sheet */
--ease-emphasized: cubic-bezier(0.2, 0, 0, 1);
--ease-emphasized-decelerate:cubic-bezier(0.05, 0.7, 0.1, 1);
--ease-emphasized-accelerate:cubic-bezier(0.3, 0, 0.8, 0.15);
/* 次要动效:按钮、芯片 */
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--ease-standard-decelerate: cubic-bezier(0, 0, 0, 1);
--ease-standard-accelerate: cubic-bezier(0.3, 0, 1, 1);
}
.modal { transition: transform .3s var(--ease-emphasized-decelerate); }
.btn { transition: background .15s var(--ease-standard); }
emphasized 系前 20% 时间走 80% 距离,感知上特别”利落”。Google 的 Pixel UI、Gmail Web 大量使用,是 Material 3 的视觉签名。
iOS 系统曲线
UIKit 的 UIView.animate(...) 默认是 easeInEaseOut,但 Apple 在 iOS 13 后引入 UISpringTimingParameters,绝大多数系统动效现在都是弹簧,不是贝塞尔。
CSS 层面没法直接写弹簧(CSS spring 提案还在草案),但可以用 cubic-bezier 近似:
:root {
/* iOS 风格的"轻微回弹"——常用在按钮按压释放 */
--ease-ios-spring: cubic-bezier(0.5, 1.5, 0.5, -0.3);
/* iOS 风格的"标准减速"——通用过渡 */
--ease-ios: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
iOS 弹簧在 y 维度会出 [0, 1] 范围,所以要确认元素有空间被弹——比如缩放从 0.95 弹到 1,中间允许过冲到 1.05;位移类似。
弹性曲线该不该用
弹性(overshoot / spring)曲线 y 值会 < 0 或 > 1:
--ease-overshoot: cubic-bezier(0.68, -0.5, 0.27, 1.5); /* 起步先后退 + 终点超过再回弹 */
--ease-anticipate: cubic-bezier(0.36, 0, 0.66, -0.5); /* 仅起步前先后退一下 */
适合的场景:
- 加入收藏:心形图标 scale 0 → 1.2 → 1
- 抽屉拉手回弹
- “成功”对勾绘制完成的小弹跳
不适合的场景:
- 任何 hover:触发太频繁,弹性会显得花哨
- 退出动画:用户预期”东西消失”,弹一下再消失反直觉
- 模态进入:稳定的减速更专业
时长对照
曲线和时长一起决定感觉。常用对照:
| 场景 | 时长 | 曲线 |
|---|---|---|
| 按钮 hover 颜色变化 | 100-150ms | standard |
| 卡片轻微抬起 | 150-200ms | standard-decelerate |
| 抽屉/Sheet 滑入 | 250-300ms | emphasized-decelerate |
| 抽屉/Sheet 滑出 | 200-250ms | emphasized-accelerate |
| 页面切换(Web) | 200-280ms | emphasized |
| 弹性反馈(点赞、收藏) | 350-450ms | overshoot |
超过 500ms 一般是动画失败——不是曲线问题,是时长本身太长。
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;
}
}
这是硬性建议——前庭功能敏感的用户会被弹性动效引发眩晕。任何弹性曲线都必须有降级路径。
一句话总结
“什么都不写”跑的是 ease,App 感来自 emphasized / spring;进入用 decelerate、退出用 accelerate、退出永远比进入快。