CSS 数字滚动动画怎么做?@property 配合滚动驱动动画完整实战

2026-09-28 0 659

数据看板上那种「数字从 0 一路跳到 1280」的效果,过去基本是 JavaScript 的保留节目:写个 requestAnimationFrame,配一条缓动曲线,每帧把插值结果写进 textContent。不算难,但每个项目复制一遍也挺烦的,而且这类代码往往写着写着就长出了一堆边界处理。

现在的 CSS 已经能把这件事整个吃下来。需要的只有三样东西:@property、CSS 计数器、滚动驱动动画。下面从原理开始拆,最后给一份能直接抄走的完整代码。

三个零件,缺一不可

先给结论,后面再展开:

  • @property 把一个自定义属性从「不可插值的字符串」升级成「可以补间的整数」;
  • counter-reset 配合 counter(),把这个整数输出成页面上的文字;
  • animation-timeline: view() 让动画进度跟着滚动位置走,而不是跟着时间走。

少任何一个,这套方案都跑不起来。

一、为什么自定义属性默认动不起来

如果你直接这么写:

@keyframes bad {
  from { --n: 0; }
  to   { --n: 1280; }
}

页面上的结果是从头到尾一点变化都没有,或者干脆在某个瞬间从 0 直接跳到 1280。原因在于,未被 @property 注册的自定义属性,在浏览器眼里只是一串未经解析的 token,跟一个字符串没什么区别。两个字符串之间没法做数值插值,动画系统只能退化成「离散跳变」——也就是在某个进度点直接换值。

@property 的作用就是提前把类型交代清楚:

@property --count {
  syntax: "<integer>";
  initial-value: 0;
  inherits: true;
}

这条规则有几个地方容易写错:

  • syntax 的值必须是带尖括号的合法类型字符串,写成 "integer" 整条规则会被静默丢弃,不报错也不生效;
  • initial-value 必须能匹配上声明的语法,整数就写 0,别加引号;
  • inherits 决定子元素能不能继承到动画中间的值。这里我们打算把动画挂在卡片上、在子元素里读值,所以必须给 true。

注册完成之后,--count 就参与了正常的整数插值流程。有意思的是,<integer> 的插值结果会被取整到整数——浏览器每一帧会把算出来的值四舍五入。这个特性放在别的场景可能是缺点,放在计数器上恰好就是我们想要的跳字效果,不用额外做任何处理。

二、把整数渲染成页面上的文字

接下来的问题是:CSS 没有提供「把变量值当文本输出」的能力。content: var(--count) 这种写法是行不通的,content 只接受字符串、图片和计数器,不认识数字。

绕过去的办法是借用计数器:

.stat-card__value {
  counter-reset: n var(--count);
}

.stat-card__value::after {
  content: counter(n);
}

counter-reset 在这里做的事是把名为 n 的计数器设成 --count 的当前值——注意它虽然叫 reset,但接受任意整数,不只是重置为零。然后 counter(n) 把这个值渲染出来。

计数器的作用域规则是「从声明它的元素开始向下」。所以 counter-reset 写在元素本体、counter() 写在它自己的伪元素里,是能正常工作的。

另外记得加上 font-variant-numeric: tabular-nums。等宽数字可以避免数字在跳变过程中左右抖动——很多等宽字体之外的字体里,1 的宽度和 8 是不一样的,数字位数一变整块文本就会晃。

三、让滚动位置充当时间轴

最后一步,把动画的驱动源从「时间」换成「滚动位置」:

.stat-card {
  animation: stat-in linear both;
  animation-timeline: view();
  animation-range: entry 15% cover 40%;
}

@keyframes stat-in {
  from { --count: 0; }
  to   { --count: 1280; }
}

view() 的意思是:这张卡片自身的可见性进度,就是这条动画的时间线。卡片从视口下方冒头的那一刻是 0%,完全离开视口是 100%。

animation-range 则决定用这段可见性区间里的哪一截来跑动画。常用的区间名有四个,在「纵向滚动、元素比视口矮」的前提下:

  • entry:元素从下方开始进入,到完全进入结束;
  • contain:元素完全落在视口内的那一段;
  • exit:元素开始离开,到完全离开;
  • cover:从开始进入一直到完全离开,是这几个里跨度最大的一个。

entry 15% cover 40% 读作:从「进入视口 15% 的位置」开始动,到「cover 区间的 40% 位置」结束。这个组合能让动画在卡片刚露出小半截的时候启动,滚动到画面中上部时收尾,节奏比较自然。

这里有个常见坑值得单独说:animation 简写会重置 animation-timeline。所以下面这段代码是无效的:

/* 错误示范:时间线被简写重置回了 auto */
.stat-card {
  animation-timeline: view();
  animation: stat-in linear both;
}

简写在 animation-timeline 之后出现,会把它重置成初始值 auto,动画就退化成按时间播放了。正确做法是把 animation-timeline 放在简写之后,或者像我后面那样单独放进 @supports 块里。

四、完整代码

把上面的片段拼起来,再加上一个和数字联动的进度条。为了让两个元素共享同一个动画进度,我额外注册了一个 --grow,由同一个关键帧一起驱动。

HTML 结构:

<div class="stat-card" role="img" aria-label="本月新增订单 1280 笔,目标达成率 78%">
  <p class="stat-card__value"></p>
  <p class="stat-card__unit">笔</p>
  <div class="stat-card__track"></div>
</div>

注意 .stat-card__value 是个空元素,内容全部由 ::after 生成。

CSS:

/* ---------- 1. 注册可以补间的自定义属性 ---------- */
@property --count {
  syntax: "<integer>";
  initial-value: 0;
  inherits: true;
}

@property --grow {
  syntax: "<number>";
  initial-value: 0;
  inherits: true;
}

/* ---------- 2. 卡片本体 ---------- */
.stat-card {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: 0 6px;
  padding: 24px 28px;
  border-radius: 16px;
  background: #0f1218;
  color: #eef2f8;
  animation: stat-in linear both;
}

/* ---------- 3. 数字 ---------- */
.stat-card__value {
  counter-reset: n var(--count);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 44px;
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.stat-card__value::after {
  content: counter(n);
}

.stat-card__unit {
  font-size: 14px;
  color: #7c8798;
}

/* ---------- 4. 进度条 ---------- */
.stat-card__track {
  grid-column: 1 / -1;
  height: 6px;
  margin-top: 18px;
  border-radius: 999px;
  background: #1c222d;
  overflow: hidden;
}

.stat-card__track::after {
  content: "";
  display: block;
  height: 100%;
  border-radius: inherit;
  transform-origin: left center;
  transform: scaleX(var(--grow));
  background: linear-gradient(90deg, #4f8cff, #8b5cf6);
}

/* ---------- 5. 关键帧 ---------- */
@keyframes stat-in {
  from {
    --count: 0;
    --grow: 0;
  }
  to {
    --count: 1280;
    --grow: 0.78;
  }
}

/* ---------- 6. 支持滚动驱动时接管时间线 ---------- */
@supports (animation-timeline: view()) {
  .stat-card {
    animation-timeline: view();
    animation-range: entry 15% cover 40%;
  }
}

/* ---------- 7. 不支持时的降级:按时间播一遍 ---------- */
@supports not (animation-timeline: view()) {
  .stat-card {
    animation-duration: 1.4s;
    animation-delay: 0.2s;
  }
}

/* ---------- 8. 尊重减少动效偏好 ---------- */
@media (prefers-reduced-motion: reduce) {
  .stat-card {
    animation: none;
    --count: 1280;
    --grow: 0.78;
  }
}

五、几个真正会绊到人的点

inherits 必须为 true

动画写在 .stat-card 上,读值的是 .stat-card__value 和 .stat-card__track::after。如果 @property 里把 inherits 设成 false,子元素拿到的永远是 initial-value,页面上的数字会一动不动地停在 0。

无障碍要给一份兜底语义

纯 CSS 拼出来的数字,在无障碍树里的表现取决于浏览器和读屏软件的配合,不太可靠。稳妥的做法是给卡片容器加 role="img" 加 aria-label,把完整信息一次性说清楚,内部的装饰性文本自动被忽略。上面示例里的 aria-label 就是这个用途。

性能上有一个躲不开的成本

滚动驱动动画本身跑在合成器线程上,很轻。但计数器不是——counter-reset 的数值每次变化都会让文本内容重新布局和重绘,这部分开销没法绕开。所以这东西适合用在页面上零星几个关键数据上,别拿它去渲染一个几十行的表格。真到了那个量级,还是交给 JavaScript 更划算。

滚动容器别搞错

view() 默认找最近的滚动容器。如果卡片外面还套了一层 overflow: auto 的滚动区域,时间线就会挂在那个容器上,而不是整个页面。这个行为大多数时候是对的,但如果发现动画触发时机不对,先检查一下是不是多了一层滚动祖先。

六、浏览器支持与降级

@property 已经全线可用:Chrome 85+、Safari 16.4+、Firefox 128+ 都支持。

滚动驱动动画这边稍微滞后一点:Chrome 和 Edge 从 115 开始支持,Safari 26 跟上了,Firefox 目前还没默认开启。

所以降级方案是必须的。上面代码里用了两个 @supports 分支:支持滚动时间线时接管时间线,不支持时退回成一条普通的定时动画。不写降级的话,那些浏览器里动画时长为 0,数字会直接跳到终值——虽然不算难看,但白白丢掉了一个动画机会。

七、还能往哪扩

这套「注册属性 + 关键帧」的组合,本质上是给 CSS 开了一个可以自由计算的数值通道。除了数字,同一个 --grow 还能顺手驱动别的东西:

  • 用 color-mix(in oklch, #4f8cff calc(var(--grow) * 100%), #8b5cf6) 让进度条颜色随进度偏移;
  • 用 conic-gradient 配 --grow 做环形进度;
  • 给多个卡片设置不同的 animation-range 起点,滚动时依次点亮,比统一触发要耐看。

再往下想,把 animation-timeline 从 view() 换成 scroll(),时间线的主体就从「某个元素何时可见」变成了「整个滚动容器滚了多远」,适合做页面顶部的阅读进度条。切换的成本只有一行代码。

CSS 数字滚动动画怎么做?@property 配合滚动驱动动画完整实战
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请发送邮件1506151422@qq.com联系,将会及时下架删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 css CSS 数字滚动动画怎么做?@property 配合滚动驱动动画完整实战 https://www.taomawang.com/web/css/2829.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务