react-bits 动画评审标准详解:从频率决策、缓动曲线到 GPU 性能的完整数值手册
在 react-bits 这类以动画为核心卖点的组件库中,"动效做得动"远远不够——动效必须在正确的频率下出现、以正确的曲线和时长运行、并且不拖慢渲染管线。仓库中的 STANDARDS.md 就是一份动画评审的精确数值参考手册(蒸馏自 Emil Kowalski 的设计工程哲学),它规定了每一条动画在 code review 中应被引用的具体曲线、时长与规则。读完本篇,你将掌握一套可直接落地的动效决策流程:何时该删掉动画、该选哪条 cubic-bezier、时长预算给多少、以及如何在 transform/opacity 与 springs 之间做出正确的技术选型。
这套标准在仓库中的定位
STANDARDS.md 位于 AGENTS/SKILLS/review-animations/ 目录下,与同目录的 SKILL.md 配套使用:后者定义了评审行为姿态(默认标记问题、"批准是靠争取的"),前者则提供评审所需的全部精确数值——"在结论中引用这些值,而不是靠估算"。
更值得注意的是,这套标准不是纸面规范:react-bits 自身的样式系统就采用了其中定义的自定义缓动曲线。在 variables.css 中可以看到三组曲线 token 与四个时长 token 的定义:
/* 自定义强缓动曲线(与 STANDARDS.md 完全一致) */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); /* 强 ease-out,用于 UI */
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); /* 强 ease-in-out,用于屏幕内位移 */
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); /* iOS 风格的抽屉曲线 */
/* 时长预算 token */
--dur-press: 120ms;
--dur-tooltip: 160ms;
--dur-menu: 200ms;
--dur-panel: 280ms;
四个时长 token 正好落在下文"时长预算"表格的区间内(按钮按压 100–160ms、菜单 150–250ms、面板 200–500ms),且全部 ≤ 300ms 的 UI 上限。搜索仓库源码可以发现,transform var(--dur-press) var(--ease-out) 这一组合被大量组件复用,例如 IconSelect.jsx、ComponentList.jsx、TabsLayout.jsx 以及 OpenInStudioButton.jsx 等,说明"按压反馈 = 短时长 + 强 ease-out + transform"是仓库内统一执行的实践。页面级转场 transitions.css 则只动 opacity(transition: opacity 300ms var(--ease-out)),同样只触及 GPU 合成属性。
该不该做动画:频率决策表
STANDARDS.md 的第一原则是按"用户看到该动效的频率"决定做不做:
| 频率 | 决策 |
|---|---|
| 每天 100+ 次(键盘快捷键、命令面板开关) | 永远不做动画 |
| 每天十几次(hover 效果、列表导航) | 删除或大幅削弱 |
| 偶尔(模态框、抽屉、toast) | 标准动画 |
| 罕见/首次(onboarding、反馈、庆祝时刻) | 可以加入惊喜感 |
永远不要为键盘触发的动作加动画——它们一天重复上百次,动画只会让它们显得迟钝、脱节。文档举的例子是 Raycast 的命令面板没有开/关动画,这对每天使用数百次的工具来说是正确的。
动效的合法目的只有五类:空间一致性(spatial consistency)、状态指示、解释说明、反馈、防止突兀变化。"看起来酷"用于高频出现的元素,不构成合法理由。
缓动:决策顺序与自定义曲线
文档给出了一条明确的缓动选择决策顺序:
- 进入或退出(entering/exiting)→
ease-out(起步快,感觉响应迅速) - 屏幕内移动/形变 →
ease-in-out - hover/颜色变化 →
ease - 恒速运动(跑马灯、进度条)→
linear - 默认 →
ease-out
两条硬规则:UI 上永远不用 ease-in——它起步慢,恰好拖慢了用户最关注的那个瞬间;同一时长下 ease-out 200ms 的"感觉速度"快于 ease-in 200ms。以及:内置 CSS 缓动太弱,要认真做动效必须使用强自定义曲线,即上文 variables.css 中的三条 cubic-bezier。文档同时建议从在线缓动曲线工具(如 easing.dev、easings.co 这类曲线库)获取曲线,不要从零手写。
时长预算:UI 动画保持在 300ms 以内
| 元素 | 时长 |
|---|---|
| 按钮按压反馈 | 100–160ms |
| Tooltip、小型 popover | 125–200ms |
| 下拉菜单、select | 150–250ms |
| 模态框、抽屉 | 200–500ms |
| 营销/解释性动画 | 可以更长 |
核心规则是 UI 动画保持在 300ms 以内:一个 180ms 的下拉菜单比 400ms 的更有响应感;同样实际的等待时间,更快的 loading 动画会让"加载"本身感觉更快;首次出现之后的 tooltip 跳过延迟和动画直接显示,会让工具栏整体感觉更快。react-bits 的 token 设计(--dur-press: 120ms、--dur-tooltip: 160ms、--dur-menu: 200ms)正是这张预算表的具体化。
物理感:起点、原点与按压反馈
-
永远不用
scale(0)。 应该从scale(0.9–0.97)+opacity: 0开始——真实世界里没有东西是"从虚无中凭空出现"的。 -
Popover 要有原点感知(origin-aware)。 缩放应从触发元素处发生,而非元素中心:
.popover { transform-origin: var(--radix-popover-content-transform-origin); } /* Radix */ .popover { transform-origin: var(--transform-origin); } /* Base UI */模态框是例外——它出现在视口中央,应保持
transform-origin: center。 -
按钮按压反馈。
:active时transform: scale(0.97),配transition: transform 160ms ease-out。幅度要克制(0.95–0.98),适用于一切可按压元素。
仓库内该模式的实际形态是 transform var(--dur-press) var(--ease-out)(120ms 落在 100–160ms 预算内),配合 scale 类的按压位移,可以在 ComponentList.jsx 和 CTA.css 等文件中找到一致的实现。
弹簧(Springs):模拟物理、无固定时长
弹簧动画因为模拟物理而显得自然:没有固定时长,而是根据参数"落定"。适用场景:带惯性的拖拽、"活的"元素(如 Dynamic Island)、可中断的手势、装饰性鼠标跟随。
// Apple 风格(更易推理)—— 推荐
{ type: "spring", duration: 0.5, bounce: 0.2 }
// 传统物理参数(控制更细)
{ type: "spring", mass: 1, stiffness: 100, damping: 10 }
要点:bounce 保持克制(0.1–0.3),大部分 UI 避免回弹,只留给 drag-to-dismiss 和俏皮交互;弹簧在中断时会保留速度(keyframes 则从零重启),因此对"用户可能中途反向操作"的手势是首选。
鼠标交互:用 useSpring 做插值,而不是把值直接绑定到鼠标坐标——直接绑定没有惯性、显得生硬。而且这仅适用于装饰性运动。
可中断性:transitions 对 keyframes
CSS transitions 可以在动画中途被中断并重新定向;keyframes 则会从零重启。对快速触发的一切(toast 连续添加、开关切换),transitions 更平滑:
/* 可中断 —— 适合动态 UI */
.toast { transition: transform 400ms ease; }
/* 不可中断 —— 动态 UI 避免使用 */
@keyframes slideIn { from { transform: translateY(100%); } to { transform: translateY(0); } }
纯 CSS 实现进入动画(无需 JS)用 @starting-style:
.toast {
opacity: 1; transform: translateY(0);
transition: opacity 400ms ease, transform 400ms ease;
@starting-style { opacity: 0; transform: translateY(100%); }
}
旧环境降级方案:useEffect(() => setMounted(true), []) + data-mounted 属性。
不对称时序:决定慢、响应快
"用户在做决定的地方慢,系统响应的地方快":
.overlay { transition: clip-path 200ms ease-out; } /* 释放:快 */
.button:active .overlay { transition: clip-path 2s linear; } /* 按压:慢而刻意 */
对"按压—释放"、"长按"这类交互,进入/退出时序对称是一个评审问题。
性能:只在 GPU 上动 transform 与 opacity
-
只动画
transform和opacity——它们跳过 layout/paint,走 GPU 合成。padding/margin/height/width/top/left会触发全部三个渲染阶段。 -
不要用父元素上的 CSS 变量驱动子元素 transform——它会让所有子元素重算样式。直接把
transform设在元素上:element.style.setProperty('--swipe-amount', `${d}px`); // 差:所有子元素重算 element.style.transform = `translateY(${d}px)`; // 好:只影响这个元素 -
Framer Motion 的简写属性不是硬件加速的。
x/y/scale走主线程 rAF,页面繁忙时会掉帧。要用完整 transform 字符串:<motion.div animate={{ x: 100 }} /> // 繁忙时掉帧 <motion.div animate={{ transform: "translateX(100px)" }} /> // 硬件加速 -
高负载下 CSS 动画胜过 JS——CSS 在主线程之外运行,而基于 rAF 的动画会在浏览器加载/脚本/绘制时卡顿。预定动作(predetermined motion)用 CSS,动态/可中断的用 JS。
-
WAAPI 同时提供 JS 控制与 CSS 级性能(硬件加速、可中断、无依赖库):
element.animate([{ clipPath: 'inset(0 0 100% 0)' }, { clipPath: 'inset(0 0 0 0)' }], { duration: 1000, fill: 'forwards', easing: 'cubic-bezier(0.77, 0, 0.175, 1)' });
Transform 与 clip-path:被低估的工具箱
translate的百分比相对元素自身尺寸——translateY(100%)移动的距离就是元素自身高度,与具体像素无关(Sonner/Vaul 就是这样定位 toast/抽屉的)。优先于硬编码 px。scale()会连带缩放子内容(字体、图标)——对按压反馈而言这正是特性。- 3D:
rotateX/Y+transform-style: preserve-3d可以在不用 JS 的情况下实现深度/环绕/翻转。 clip-path: inset(t r b l)是强力的动画工具,每个值从对应方向"吃掉":滚动显现(inset(0 0 100% 0)→inset(0 0 0 0))、长按删除遮罩、无缝的 tab 颜色过渡(复制一份再裁剪活动副本)、对比滑块。
手势与拖拽:像真实物体一样响应
- 基于速度的惯性关闭:不要要求拖过距离阈值——计算速度
Math.abs(distance)/elapsedMs,大于约 0.11 即关闭。一次 flick 就应该足够。 - 边界阻尼(damping):越过自然边界继续拖时,拖得越远移动越少——真实物体在停下前会减速。
- Pointer capture:拖拽开始后捕获指针,指针移出边界后拖拽仍然继续。
- 多点触控保护:拖拽开始后忽略额外的触点(
if (isDragging) return),防止位置跳变。 - 用摩擦代替硬墙——允许过冲(over-drag)并伴随阻力递增,而不是一堵看不见的墙。
用模糊掩盖不完美的交叉淡入淡出
即使调遍了缓动和时长,crossfade 仍然显示出两个重叠状态时,在过渡期间叠加轻微的 filter: blur(2px),把两个状态融合成一次被感知的单一形变。注意 blur 保持在 20px 以下——重模糊非常昂贵,在 Safari 上尤其如此。
错峰进场(Stagger)
成组进入要错峰,条目间隔 30–80ms;更长的延迟只会显得慢。Stagger 是装饰性的——永远不要在它播放时阻塞交互:
.item { opacity: 0; transform: translateY(8px); animation: fadeIn 300ms ease-out forwards; }
.item:nth-child(2) { animation-delay: 50ms; }
.item:nth-child(3) { animation-delay: 100ms; }
@keyframes fadeIn { to { opacity: 1; transform: translateY(0); } }
可访问性:更少更柔和,而不是零
@media (prefers-reduced-motion: reduce) {
.element { animation: fade 0.2s ease; } /* 保留 opacity/颜色,去掉 transform 位移 */
}
@media (hover: hover) and (pointer: fine) {
.element:hover { transform: scale(1.05); } /* 门控 hover 动效——触屏会在点击时触发伪 hover */
}
const reduce = useReducedMotion();
const closedX = reduce ? 0 : '-100%';
Reduced motion 的含义是更少、更柔和的动画,而不是完全没有:保留有助于理解的过渡(透明度、颜色),移除位移/位置变化。仓库中大量组件页面(如 Plasma.jsx、DepthText.css 等)都包含 prefers-reduced-motion 处理分支,可搜索 prefers-reduced-motion 查看覆盖面。
调试手段:感觉不对时怎么办
- 慢放:把时长放大 2–5 倍,或用 DevTools 的 animation inspector。检查颜色是否干净地交叉淡化、缓动是否突然中止、
transform-origin是否正确、联动属性是否保持同步。 - 逐帧检查:Chrome DevTools 的 Animations 面板能暴露联动属性之间的时间漂移。
- 真机:手势类(抽屉、滑动手势)必须连手机、通过 IP 访问开发服务器、用 Safari 远程调试来验证。
- 第二天再看:开发当下看不出的瑕疵,次日的新眼睛会立刻发现。
整体感(Cohesion):动效要匹配组件性格
动效要与组件性格一致:俏皮的产品可以更弹,专业仪表盘应该干净利落、干脆快速。Sonner 之所以"感觉对",部分原因在于它的缓动、时长、设计甚至名字彼此协调——刻意略慢、用 ease 而非 ease-out 以获得优雅感。至于进入/退出列表中 opacity + height 的组合,没有公式,只能靠试错调到"感觉对"为止。
配套:评审技能如何把这些标准变成可执行的 Block/Approve
SKILL.md 定义了如何使用这份标准做评审:十条不可妥协的标准(有目的的动效、频率匹配、响应式缓动、300ms 以内 UI、原点与物理正确性、可中断性、仅 GPU 属性、可访问性、不对称进出、整体感),一组"看到即标记"的升级触发器(transition: all、UI 上的 ease-in、高频动作上的动画、动画化的布局属性、Framer Motion 简写属性、缺失 prefers-reduced-motion 等),以及一个修复优先级阶梯:
- 删掉动画(高频/无目的/键盘触发)→ 2. 削弱它 → 3. 修缓动 → 4. 修原点/物理性 → 5. 改成可中断 → 6. 移到 GPU → 7. 不对称时序 → 8. 打磨(模糊遮罩、stagger、
@starting-style、弹簧)→ 9. 可访问性与整体感。
输出格式也是固定的:一张 Before/After/Why 的发现表格,加上按影响分层的结论(感觉崩坏 > 错失的简化 > 性能 > 可中断与时序 > 原点/物理/整体感 > 可访问性),最后给出明确的 Block 或 Approve 决定——需要精确数值(曲线、时长、弹簧配置)时,一律从 STANDARDS.md 引用,而非估算。同仓库的 AUDIT.md 还提供了同一套标准的"审计视角"版本,八个审计类别各自附带"重点排查什么"的清单,可作为对既有代码库做动效体检时的操作手册。
小结
这份标准的价值在于把"动效好不好"从主观品味问题变成了可引用、可验证的工程问题:频率表决定做不做,决策顺序 + 三条强 cubic-bezier 决定怎么动,预算表决定动多久,transitions/弹簧/@starting-style 决定可否被打断,transform/opacity + WAAPI 决定不掉帧,最后由 prefers-reduced-motion 与组件性格兜底。对 react-bits 这样的动画组件库而言,这套数值手册既是评审基准,也是仓库自身样式 token(见 variables.css)背后的设计依据——照着它执行,"感觉对"就有了可复制的工程路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00