滚动触发数字计数特效(自研 JS,时长/起始值/重复触发可调)
滚动触发数字计数:数字进入视口时从 0 平滑递增到目标值,支持小数/千分位/后缀,动画时长与起始值可调,可一次性或重复触发,零依赖。
效果演示(真实可交互)
🎯 向下滚动,数字进入视口时从 0 开始计数
继续向下滚动 ↓
0精选组件
0好评率
0注册用户
0成立年份
↑ 数字已触发
演示区用内嵌滚动容器模拟页面滚动;正式使用时把
root设为null即作用于页面视口。
完整代码
<div class="stat" data-target="1024" data-suffix="+"><b>0</b><span>精选组件</span></div>
<div class="stat" data-target="98.6" data-decimals="1" data-suffix="%"><b>0</b><span>好评率</span></div>
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
function runCount(el, dur = 1800) {
const target = parseFloat(el.dataset.target);
const decimals = parseInt(el.dataset.decimals || '0', 10);
const suffix = el.dataset.suffix || '';
const b = el.querySelector('b');
const t0 = performance.now();
const fmt = v => decimals === 0
? Number(v.toFixed(0)).toLocaleString('en-US') // 千分位 36,000
: v.toFixed(decimals);
const tick = now => {
const t = Math.min((now - t0) / dur, 1);
b.textContent = fmt(target * easeOutCubic(t)) + suffix;
if (t < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
const observer = new IntersectionObserver(entries => {
for (const en of entries) {
if (en.isIntersecting) {
runCount(en.target);
observer.unobserve(en.target); // 去掉这行 = 每次进入视口都重新计数
}
}
}, { threshold: 0.5 });
document.querySelectorAll('.stat').forEach(el => observer.observe(el));
数据驱动:目标值、小数位、后缀全部写在 data-* 属性里,HTML 里直接配数据,JS 零改动即可复用。
参数说明
| 参数 | 位置 | 说明 |
|---|---|---|
| 目标值 | data-target | 计数终值,支持整数与小数 |
| 小数位 | data-decimals | 0 时自动加千分位分隔符 |
| 后缀 | data-suffix | 如 +、%、万,显示在数字后 |
| 动画时长 | runCount(el, dur) | 300-4000ms,默认 1800ms |
| 起始值 | 计数起点 | 演示面板可调,默认从 0 开始 |
| 触发阈值 | threshold: 0.5 | 元素可见一半才触发 |
| 触发模式 | unobserve() | 一次触发 / 移除后重复触发 |
兼容性与性能
- 核心动画用
requestAnimationFrame驱动 +easeOutCubic缓动函数,视觉上「先快后慢」,最符合数据呈现习惯 IntersectionObserver原生 API,无需监听 scroll、无主线程抖动;触发后即停止观察,长页面多组数字也只消耗一次回调- 数字用
font-variant-numeric: tabular-nums可避免计数过程中数字宽度跳动(演示已内置) - 全现代浏览器兼容,移动端表现同样流畅;与 countUp.js 相比零依赖、体积不足其十分之一
常见问题
Q: 和 countUp.js 比有什么优势? countUp.js 虽在安全池,但本组件为粤喵自研实现:数据走 data-* 属性声明、自带千分位与小数格式化、按需观察自动释放,代码量更少且行为完全可控,无版本升级风险。
Q: 数字到顶后想四舍五入而不是截断怎么办? 用 Math.round(v) 替代 toFixed(0) 即可;小数场景 toFixed(decimals) 本身已按四舍五入处理。
Q: 同一个数字想每次滚动进入都重新计数? 去掉 observer.unobserve(en.target),并在元素离开视口时把 <b> 内容重置为起始值(演示面板勾选「仅触发一次」即可对比两种模式)。
Q: 可以商用吗? 可以。本组件为粤喵自研原创实现,免费商用无需署名,可自由修改。
许可与来源
- 许可证:自研原创 —— 粤喵自研原创,可免费用于个人与商业项目,无需署名。查看来源公示与许可清单
- 来源:粤喵团队原创自研(从效果思路到代码实现均为独立完成,未直接复制第三方代码)
- 首次发布:2026-09-01 · 最近更新:2026-09-01
- 修改日志:2026-09-01 首次发布;2026-09-13 补充许可证与来源标注、复制按钮说明与法务页入口
- 纠错:联系页面 / min@cactus.asia,2 个工作日内核实处理
常见问题
这个组件可以商用吗?
可以。本组件为粤喵自研原创,可免费用于个人和商业项目,无需署名。
浏览器兼容性如何?
支持所有现代浏览器(Chrome / Firefox / Safari / Edge 最新版本)。旧版 IE 不支持 CSS 动画相关特性。
怎么修改动画参数?
直接调整 CSS 中的 animation-duration(时长)、animation-timing-function(缓动曲线)等属性即可,代码中有注释说明。