动效判断
默认答案是「不动」。 动效只有在能解释状态变化、或表达物理因果时才加。加得对,界面显得贵;加得错,界面显得廉价——而且廉价得更明显。
最重要的信号:如果一个动效让人注意到「这里有个动画」,它就是错的。好的动效是让人注意到「这件事发生了」。
一、先问四个问题
按顺序,任何一个答不上来就不做。
| # | 问题 | 答不上来意味着 |
|---|---|---|
| 1 | 这个动效在解释什么?(从哪来、到哪去、什么变了) | 纯装饰,删掉 |
| 2 | 用户在这个动效期间能操作吗? | 可能是阻塞式动画,需要重新设计 |
| 3 | 它多频繁出现? | 高频(> 每天几十次)→ 时长要更短或干脆不做 |
| 4 | 关掉它,用户会困惑吗? | 不会困惑 → 它是装饰,删掉 |
高频交互的动效要短到几乎察觉不到(80–140ms)。一天点 200 次的按钮,每次 300ms 的动画等于浪费用户一分钟。
二、什么时候该动
| 目的 | 该做 | 例子 |
|---|---|---|
| 解释空间关系 | ✅ 必须做 | 弹层从触发它的按钮方向长出;抽屉从边缘滑入 |
| 确认操作已生效 | ✅ 必须做 | 按钮按下反馈、提交成功、复制成功 |
| 引导注意力 | ✅ 谨慎做 | 新内容进入、错误提示 |
| 表达物理真实 | ✅ 谨慎做 | 拖拽跟手、甩出带惯性、边界回弹 |
| 保持连续性 | ✅ 必须做 | 列表项增删时其他项平滑移动;布局切换不跳 |
| 纯装饰 | ❌ 不做 | 元素进视口就淡入上移、logo 旋转、背景粒子 |
| 掩盖加载 | ⚠️ 有条件 | 优先用骨架屏(有信息),而不是转圈(无信息) |
三、动什么属性
只动这两类:
| 类别 | 属性 | 说明 |
|---|---|---|
| 合成器属性(首选) | transform(translate/scale/rotate)、opacity | 不动布局,可跑在合成线程 |
| 可接受 | filter(blur/brightness)、clip-path、color、background-color | 视情况,注意代价 |
| 禁止 | width、height、top/left、margin、padding、box-shadow(大范围) | 触发布局/重绘 |
需要动尺寸时,用 transform: scale() 替代宽高,或先用 grid-template-rows: 0fr → 1fr 做高度过渡。
永远不要用 transition: all。 它会把你不想要的东西一起动了,并且在需求变化时产生意外。
css/* 错 */ transition: all 0.3s ease-in-out; /* 对 */ transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1), opacity 180ms cubic-bezier(0.22, 1, 0.36, 1);
四、时长
| 距离 / 尺度 | 时长 | 说明 |
|---|---|---|
| 微反馈(按下、勾选、切换) | 80 – 140ms | 必须快到像即时 |
| 小元素(tooltip、下拉、标签) | 140 – 200ms | |
| 中元素(卡片、面板、抽屉) | 200 – 300ms | |
| 大元素(模态、整页转场) | 300 – 420ms | |
| 超大(全屏叙事) | 420 – 600ms | 超过 600ms 一定拖沓 |
| 退出 | 比进入快 30–50% | 用户已经决定离开了,别让他等 |
核心规则
- 越近越快。 元素移动的物理距离越大,时长越长(近似线性,约 1ms / 1px,量级参考)。
- 退出比进入快。
- 高频比低频快。
- 超过 400ms 的动画,用户开始感觉界面变慢。
五、缓动
css:root { /* 标准:进入/退出,快起慢收,最通用 */ --ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* 进入屏:从静止起步 */ --ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* 双向:位置移动、拉伸 */ --ease-inout: cubic-bezier(0.65, 0, 0.35, 1); /* 强调:弹层出现、需要一点俏皮 */ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); }
| 场景 | 曲线 |
|---|---|
| 元素进入(出现、展开) | --ease-out(快起慢收) |
| 元素退出(消失、收起) | --ease-in(慢起快收) |
| 位置移动 / 尺寸变化 | --ease-inout |
| 弹层、需要个性 | --ease-spring,且回弹幅度要小 |
| 拖拽跟手 | 无缓动(1:1 跟手) |
| 甩出惯性 | 弹簧或 decay |
禁止 linear(除进度条、加载旋转外)和浏览器默认的 ease-in-out。默认曲线是「没想过」的证据。
弹簧参数(如果用弹簧)
交互跟手 / 拖拽: stiffness 400–600, damping 30–40, mass 1 弹层出现: stiffness 300–400, damping 28–34 轻微俏皮: stiffness 250–350, damping 18–24 (回弹 1–2 次,不要更多)
回弹超过 2 次就是「玩具感」。高客单/专业产品用 damping 高一些,几乎不回弹。
六、进入 / 退出 / 打断
进入
- 用
--ease-out,时长按尺度取。 - 方向要有意义:弹层从触发按钮的方向长出;从右侧滑入的抽屉,退出也要回右侧。
- 不要所有东西都「淡入 + 上移 20px」。这是默认解,做多了等于没有动效设计。
退出
- 比进入快 30–50%。
- 不要在退出时做复杂的编排,用户已经在看下一个东西了。
- 退出必须真的移除元素(
display: none或从 DOM 移除),不要留下不可见的可点击区域。
打断(最重要、最少人处理)
用户在动画途中再次操作时:
- 不要阻塞。 输入必须始终可响应。
- 从当前位置重新开始,不要跳回起点。用
Element.animate()的 currentTime,或读getComputedStyle的当前 transform 作为起点。 - 可中断的动效不要用 CSS
transition(无法优雅中途转向),改用 Web Animations API 或 JS 弹簧。 - 拖拽中途松手:从当前速度接续惯性,不要从零加速。这就是「跟手」和「卡」的区别。
js// 从当前状态继续,而不是把元素拉回起点 const el = document.querySelector('.sheet'); const current = getComputedStyle(el).transform; // 读取中断时的位置 el.style.transform = current; requestAnimationFrame(() => el.classList.remove('open'));
七、reduced-motion
这不是可选项。 前庭功能障碍用户会因为视差和大幅位移产生生理不适。
css@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; } }
但要有策略地降级,不是全删:
| 原动效 | 降级为 |
|---|---|
| 位移进入 | 纯透明度变化(opacity 仍在,且有意义) |
| 视差 / 滚动联动 | 完全静态 |
| 弹簧 / 回弹 | 直接到位,无缓动 |
| 自动轮播 | 停止自动播放 |
| 加载旋转 | 保留(这是必要信息) |
判断标准:降级后,信息不能丢失。位移用来表达「从哪来」时,必须换一种方式表达(例如直接出现 + 高亮)。
八、性能
- 只动
transform/opacity;需要提前提示浏览器用will-change: transform,但用完要移除(长期挂着会吃内存)。 - 滚动相关动效:用
IntersectionObserver(进入/离开)和scroll-timeline,不要在scroll事件里直接读scrollTop改样式。 - 滚动监听如果必须用,配
passive: true,并在requestAnimationFrame里合并更新。 - 目标:动画期间保持 60fps,单帧预算 16.7ms。长任务会让动画掉帧。
- 内容进入视口的滚动揭示动效:
translateY ≤ 12px、opacity 起始 ≥ 0(不要从 0 开始,会闪),时长 ≤ 400ms。 - 首屏关键内容不要用动效延迟。 LCP 元素加了
animation-delay会直接拉低性能评分。
性能预算
不谈数字的性能建议等于没有建议。以下是可验收的线:
| 指标 | 目标 | 动效相关的常见违规 |
|---|---|---|
| LCP | < 2.5s | 首屏元素带 animation-delay;展示字体延迟加载导致文字后到 |
| INP | < 200ms | 长驻动效循环占主线程;scroll 事件里同步读写样式 |
| CLS | < 0.1 | 动效改变元素尺寸(动 height);字体加载后回流 |
| 动画帧率 | 60fps(单帧 ≤ 16.7ms) | 动 width/height/top/left;大面积 filter: blur() |
| 长驻动画数量 | 同一屏 ≤ 1 个 | 多个 CSS 无限循环同时跑 |
| 字体加载 | font-display: swap + 预加载关键字体 | 无 font-display,文字在字体到达后整体位移 |
测量,而不是声称。 说「优化了性能」之前,先给出改前改后的实测值(Chrome DevTools Performance 录制,或 PerformanceObserver 读 LCP/INP/CLS)。测不了就写「未验证」。
不要为了让回答显得果断而编一个时长
遇到「这里到底该 200ms 还是 300ms 说不准」的情况:
- 正确:给出一个范围(「200–300ms 之间,我倾向 240」),并说明需要实机验证。
- 正确:写进「没验证的部分」。
- 错误:精确到「237ms」来显得有把握。
编出来的精确值会让后面所有人以为它经过验证。不确定就说不确定,这比一个假的精确值有价值。
九、自检
- 每个动效都能回答「它在解释什么」吗?
- 有没有出现
transition: all? - 退出是否比进入快?
- 用了
linear或默认ease-in-out吗? - 高频交互的时长是否 ≤ 140ms?
- 动画途中再点一次,会跳回起点吗?(会 = 不合格)
-
prefers-reduced-motion处理了吗?降级后信息还完整吗? - 只动了
transform/opacity吗? -
will-change有没有留在元素上不撤? - 首屏 LCP 元素有没有被动画延迟?


