缓动曲线选 ease-in-out 就够了?M3 与 iOS 的真实做法

· 约 3 分钟 📈 缓动曲线

写动效最容易翻车的不是时长,是曲线。同一个 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直觉
linear0, 0, 1, 1匀速,像机械
ease0.25, 0.1, 0.25, 1浏览器默认,老气
ease-in0.42, 0, 1, 1加速冲出
ease-out0, 0, 0.58, 1减速落地
ease-in-out0.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-150msstandard
卡片轻微抬起150-200msstandard-decelerate
抽屉/Sheet 滑入250-300msemphasized-decelerate
抽屉/Sheet 滑出200-250msemphasized-accelerate
页面切换(Web)200-280msemphasized
弹性反馈(点赞、收藏)350-450msovershoot

超过 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、退出永远比进入快。

❓ 常见问题

transition: 0.3s 不写曲线时默认是 linear 吗?

不是。transition-timing-function 的 CSS 初始值是 ease,对应 cubic-bezier(0.25, 0.1, 0.25, 1)——开始稍快、结束很慢。这条曲线是 1990 年代浏览器拍脑袋定的,今天看略显"老气",但绝大多数项目省略不写时跑的就是它。所以"什么都不写"和"写 linear"完全是两个动效。

ease-in / ease-out / ease-in-out 应该怎么选?

三条经验:进入屏幕用 ease-out(快速冲入,缓缓停下,像物体落地),退出屏幕用 ease-in(缓缓启动,加速离开,像被风吹走),屏幕内移动用 ease-in-out(两端慢中间快)。错配立刻会"违和"——比如对话框淡入用 ease-in,会出现"先迟疑一下再突然出现"的诡异感。

Material 3 的 emphasized 曲线为什么这么夸张?

M3 emphasized cubic-bezier(0.2, 0, 0, 1)前 20% 时间走完 80% 距离,剩下 80% 时间慢慢减速到位。它在感知上特别"快"——用户 0.1 秒就看到位置变了,但实际过渡还有 0.3 秒收尾,让眼睛跟得上。Google 官方建议用在"主导界面变化"上(页面切换、抽屉展开),普通 hover 别用,会显得花哨。

spring / overshoot 这类"弹性"曲线什么时候用?

弹性曲线(y 出 [0,1] 范围)适合"成功"反馈——加入购物车、收藏、勾选。它给用户"啵叽一下"的物理触感。但不要用在频繁触发进入/退出动画上:弹性会让"消失"变成"先被弹一下再消失",破坏空间逻辑。每个页面里弹性元素 1-2 个就够了。

📈 打开 缓动曲线 cubic-bezier 可视编辑·动画预览·iOS/Material 预设·一键复制