返回效果库

数字滚动

Animated Number

数字不是跳过去的,是一路数过去的

完整版:适合从零新建组件,规格、契约、验收全都在。

实时预览

文字
本月营收实时
¥0
420ms · 0 位小数千分位开

连点刷新:从当前显示值改道重数

点「重播」再看一次检测到「减少动态效果」,动效已降级

参数

边调边看,提示词同步更新。

角色

你是一名资深前端工程师,工作在现有的项目中,技术栈是 React 19 + TypeScript + Tailwind CSS v4(不需要动画库)。只实现下面这一个微交互:不要加额外功能,不要顺手重构周围代码。

任务

实现数字滚动:目标值变化时,用 420ms 的 requestAnimationFrame 补间从当前显示的数值数到新值,进度由手写的 cubic-bezier(0.2, 0.8, 0.2, 1) 曲线给出。补间中途换目标必须从当前显示值重新开始;NaN / Infinity 等非有限值原样显示;最后一帧必须精确等于目标值。

文件契约

- 只新建一个文件:components/effects/AnimatedNumber.tsx。

- 依赖:无;rAF 与缓动求解都手写,不要引入动画库。

- 不要修改 package.json、全局 CSS、Tailwind 配置或任何其他已有文件。

实现要点

- 文件顶部加 "use client";同一个文件里导出 useAnimatedNumber(target, options) hook 与 <AnimatedNumber value duration precision separators /> 组件。

- hook 用 ref 保存当前显示值、起始值、起始时间与 rAF id;target 变化时把「当前显示值」作为新起点(中途换目标从当前值改道),再开一轮 requestAnimationFrame。

- 进度按自实现 cubic-bezier(0.2, 0.8, 0.2, 1) 求值:先 Newton 迭代(最多 6 次、ε=0.000001)解 x → t,再算 y;每帧 value = from + (to − from) × y,进度到 1 时直接写 to。

- 格式化:非有限值原样 String(value);否则 toFixed(precision) 后按 separators 插千分位,并用 tabular-nums 固定数字宽度。

- 卸载时 cancelAnimationFrame;prefers-reduced-motion 命中时一次写入终值、不启动 rAF;首帧不播动画,只响应之后的 value 变化。

规格 —— 精确数值,不许四舍五入或“优化”

- 补间时长:420ms;最后一帧必须精确落在目标值上,不能停在四舍五入后的中间值。

- 进度曲线:手写 cubic-bezier(0.2, 0.8, 0.2, 1)——控制点 P1 (0.2, 0.8)、P2 (0.2, 1)。用 Newton-Raphson(最多 6 次迭代、ε=0.000001)解 x(t) = progress 得到 t,再取 y(t),y 就是缓动后的进度。

- 每帧数值:value = from + (to − from) × y。from 取补间开始时屏幕上显示的值;如果这个值是非有限值(上一目标刚透传过 NaN / ±Infinity),from 取 0。

- 格式化:toFixed(0)(小数位 0),千分位开启,整数部分每三位插一个 ,;非有限值直接 String(value)("NaN"、"Infinity"、"-Infinity"),不要格式化。

- 中断重启:补间中新目标到来时先取消未执行的帧,再从当前显示值重新开始;完整跑满 420ms,不要扣掉已经过去的时间。

- 显示盒:font-variant-numeric: tabular-nums、右对齐、min-width 9ch;只改文字内容——不要动画布局,也不要加 transform。

行为

- 状态机(文字状态图):

idle —(target 变为有限值且非 reduced-motion)→ tweening { from: 当前显示值, start: now }

tweening —(target 再次变化)→ tweening { from: 当前显示值, start: now }(先取消未执行的帧)

tweening —(elapsed ≥ duration)→ settled { display: target }(写入终值并停止)

idle | tweening —(target 非有限:NaN / ±Infinity)→ passthrough { display: String(target) }

tweening —(卸载)→ cancelled(cancelAnimationFrame,之后不再 setState)

reduced-motion:idle | passthrough —(target 变化)→ settled(≤1ms 内写入终值,绝不启动 rAF)

- 首帧不播动画;只有之后的 value 变化才启动补间。

- 连续改目标必须顺滑:每次新目标都读取补间中正在显示的数值,而不是上一个目标值。

- 宿主布局保持稳定:等宽数字、右对齐、固定宽度下限;不要动画 width / height / top / left。

- aria-label / role / 可见文案属于外层 UI:不要新增、改名或删除。

- 卸载时清理:取消 animation frame;不要留下定时器,不要绑 document/window 监听,卸载后不要更新状态。

禁止

- 不要改变交互语义、可访问名称(aria-label / role / 可见文案)或请求时机;只实现视觉动效。

- 不要逐帧动画 width / height / top / left;数字只更新文本内容。

- 不要用链式 setTimeout / setInterval 拼补间——用 requestAnimationFrame(或目标栈的补间引擎)。

- 不要新增依赖,也不要升级已安装的依赖。

- 不要改动全局 CSS、Tailwind 配置或上面文件之外的任何文件。

- 不要留 TODO、不要写 // 其余代码、不要用 any、不要 console.log、不要留注释掉的实验代码。

- 不要引入任何动画库——GSAP、Motion 都不行。

验收 —— 回答前逐项自查

1. 补间时长严格等于 duration(默认 420ms):200ms / 1200ms 两端都能正常起止;小数位与千分位显示符合 precision / separators。

2. 从旧值数到新值时,60–80ms 处的显示值严格大于起始值且严格小于结束值,不是瞬间跳变。

3. 结束帧显示值精确等于目标值;没有残留的 rAF,也没有残留的内联 transform / 动画样式。

4. 数字使用 tabular-nums 且容器宽度固定:滚动全程 boundingBox 稳定,不抖动、不换行。

5. prefers-reduced-motion 时目标变化在 ≤1ms 内直接落到最终值,不启动 rAF。

6. 键盘可完成 Demo 里的全部操作(Tab 聚焦、Enter / Space 触发),焦点环清晰可见。

7. 0 console 报错;中途切换目标不抛错;卸载后不再更新状态、没有残留的 rAF 回调。

输出

- 用一个代码块输出完整文件内容,不要 diff、不要省略号,代码块之前不要解释。

- 如果缺少必要依赖,先停下来问我,不要擅自安装。

- 代码块之后最后输出一行:self-check: 1 ✓ · 2 ✓ · 3 ✓ · 4 ✓ · 5 ✓ · 6 ✓ · 7 ✓,未通过的项用 ✗ 并附一句原因。

与演示同源生成3,279 字符 · 911 约 tokens · 62 行
技术规格

金额、指标或计数器换值时,用 420ms 的 rAF 补间把旧值数到新值;进度来自手写的 cubic-bezier(0.2, 0.8, 0.2, 1) 曲线(Newton 迭代反解 t)。动画途中再换目标会从当前显示值改道重数,NaN / Infinity 等非有限值原样透传。

补间时长
420 ms
小数位
0 dec
千分位
开启
缓动
cubic-bezier(0.2, 0.8, 0.2, 1)
补间
420ms · 逐帧
格式
0 位小数 · 千分位开
验收清单

提示词要求 AI 在输出末尾逐条自检。

  1. 补间时长严格等于 duration(默认 420ms):200ms / 1200ms 两端都能正常起止;小数位与千分位显示符合 precision / separators。
  2. 从旧值数到新值时,60–80ms 处的显示值严格大于起始值且严格小于结束值,不是瞬间跳变。
  3. 结束帧显示值精确等于目标值;没有残留的 rAF,也没有残留的内联 transform / 动画样式。
  4. 数字使用 tabular-nums 且容器宽度固定:滚动全程 boundingBox 稳定,不抖动、不换行。
  5. prefers-reduced-motion 时目标变化在 ≤1ms 内直接落到最终值,不启动 rAF。
  6. 键盘可完成 Demo 里的全部操作(Tab 聚焦、Enter / Space 触发),焦点环清晰可见。
  7. 0 console 报错;中途切换目标不抛错;卸载后不再更新状态、没有残留的 rAF 回调。

待回归验证提示词与演示已同源生成;尚未在真实 Cursor / Claude Code 项目中做回归。

提示词如何还原这个效果

不是把效果描写一遍,而是把效果写成一份可执行的工程需求。

数字规格
每个像素、每个时间参数都来自同一个参数源——滑块改什么,提示词就写什么。
文件契约
只允许新建指定文件、指定依赖白名单,避免它顺手改你的全局样式或装一堆包。
验收清单
提示词末尾强制 AI 自检布局位移、键盘可用、reduced-motion 等 7 项。
完整版 / 中文

React + TypeScript