CSS @property 不只是变量:从数字滚动到渐变动画,让浏览器自己算过渡

2026-09-08 0 317

我一直觉得CSS里的普通变量有个让人抓狂的毛病:它没法“渐变”。你定义了一个--x: 1,然后想把它从1过渡到100,结果页面瞬间变成100,中间一步都没有。尤其是想用CSS变量驱动动画或者绘制进度环的时候,你只能借助JavaScript去逐帧更新。

但你要是用了@property这个进阶能力,情况就完全不一样了。它相当于给自定义属性开了个后门,让浏览器能真正理解这个变量到底是一个颜色、一个角度,还是一个数字。一旦理解了,它就会帮你在过渡和动画的每一帧里“插值”。

这篇文章我不会只贴一段炫技代码,而是拿两个我能想到最常用的场景,带你把@property用明白。

先说说普通变量到底输在哪儿

看这个例子:

:root {
  --scale: 1;
  --color: #ff0000;
}
.el {
  transform: scale(var(--scale));
  transition: --scale 0.3s ease;
}
.el:hover {
  --scale: 1.5;
}

你觉得鼠标悬停时,--scale会从1平滑变化到1.5吗?并不会。因为浏览器默认自定义属性是“字符串”,无法判断该用什么方式去插值。它只知道起始值是"1",结束值是"1.5",字符串没有中间状态,那干脆直接切换。

@property的用法是让你提前告诉浏览器这个变量是什么类型。比如:

@property --scale {
  syntax: "<number>";
  inherits: false;
  initial-value: 1;
}

声明了syntax是数字之后,浏览器才会把--scale当作真正的数值去处理,碰到transition或animation,就能计算每一帧的绝对值了。

案例1:一个流畅的渐变色旋转

前几年有个非常火的“旋转边框”效果:一个容器外面有一圈渐变色,并且这圈渐变还像跑马灯一样不断转。传统做法是用伪元素加强几个动画,或者用background-position硬撑。实际上用@property配合<angle>,能让整个事情简单且顺滑很多。

基础HTML长这样:

<div class="gradient-card">Houdini 让渐变活起来</div>

先注册一个角度变量:

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

然后给卡片用上这个变量:

.gradient-card {
  --angle: 0deg;
  width: 260px;
  padding: 20px;
  text-align: center;
  border-radius: 20px;
  background: linear-gradient(100deg, #8b5cf6, #ec4899, #f59e0b);
  /* 这里我们先用普通底色占位,后面用伪元素把渐变转起来 */
}

.gradient-card::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(var(--angle), #8b5cf6, #2dd4bf, #f43f5e);
  z-index: -1;
}

接着只写一个动画:

.gradient-card {
  animation: spin 3s linear infinite;
}

@keyframes spin {
  from {
    --angle: 0deg;
  }
  to {
    --angle: 360deg;
  }
}

如果不用@property,这个--angle的动画是无法生效的。现在你把代码跑一下,能看到那一圈渐变真的像水波一样平滑转起来了。

实际工作中,这种渐变卡片可以放在登录页、标题背景,还有人拿它做加载进度条。

案例2:数字滚动动画,这次不发散

做数据可视化页的时候,经常会遇到一个需求:统计数字从0开始滚到某个目标值。以前我第一时间想到的是引入一个第三方动画库,或者用requestAnimationFrame算。现在用原生CSS就能搞定,而且效果特顺滑。

要滚动的数字在HTML里是纯语义标签:

<span class="counter" style="--target: 100"></span>

CSS侧思路是这样的:把--target设定为目标值,再用@property注册一个内部计数变量--num,用animation把它从0过渡到100,最后通过counter-reset--num显示出来。

先注册变量:

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

然后这样写计数器:

.counter {
  counter-reset: num var(--num);
  font-size: 60px;
  font-weight: 700;
  animation: number-count 2s ease-out forwards;
}

.counter::after {
  content: counter(num);
}

@keyframes number-count {
  from {
    --num: 0;
  }
  to {
    --num: 100;
  }
}

这里面微妙的地方在于counter-reset可以接收CSS变量作为值。当--num在动画中被浏览器计算成一个个整数,比如23.6,由于syntax是<integer>,浏览器会自动取整,然后把它显示在伪元素里。

如果你想显示“1234”这种更复杂的数字,只需要把目标值写到HTML中,再配合animation-delay或者给不同元素设置不一样的目标值。举个常见的带进度数字的按钮:

<button style="--target: 85"></button>
.download-btn {
  --target: 85;
  counter-reset: downloadNum var(--num);
}
.download-btn::after {
  content: counter(downloadNum) "%";
  animation: downloadAnim 1.5s ease-out forwards;
}
@keyframes downloadAnim {
  from { --num: 0; }
  to { --num: var(--target); }
}

这里又有一个比较现代的点:动画的结束值可以是另一个普通CSS变量。因为--target在按钮上通过内联方式设置了,而--num是一个已注册的整数,两者可以互通。

背景颜色过渡的进阶玩法

类似的,如果你想在鼠标悬停时,让某一个块从纯蓝色平滑过度到青色,通常你得写transition: background 0.3s。但有时候你是用两个半透明的渐变叠加,你会发现背景颜色过渡不够细腻。这时候用@property把颜色变量也注册成<color>类型:

@property --bg-start {
  syntax: "<color>";
  inherits: false;
  initial-value: #3498db;
}

@property --bg-end {
  syntax: "<color>";
  inherits: false;
  initial-value: #2ecc71;
}

然后你可以定义一组背景色,并在hover时改变值

.btn {
  background: linear-gradient(135deg, var(--bg-start), var(--bg-end));
  transition: --bg-start 0.3s ease, --bg-end 0.3s ease;
}
.btn:hover {
  --bg-start: #e74c3c;
  --bg-end: #f39c12;
}

单看transition这行你可能觉得神奇属性怎么还能直接transition?其实不是的,transition的属性名可以是自定义属性。前提是它被@property注册成合法类型。如果没注册,写什么都不好使。

兼容性以及回退方案

现在的Chrome、Edge和Safari都已经支持了@property,Firefox也从2024年开始对应实现了。不过要在生产环境用的话,我建议你加一个柔和降级策略。用一个@supports判断浏览器是否认得@property的语法:

@supports (syntax: "<color>") {
  @property --color-primary {
    syntax: "<color>";
    inherits: false;
    initial-value: #4a90e2;
  }
}

@supports不能完全检测一个变量有没有注册成功。真正稳妥的办法是先用CSS变量设置默认值,然后在支持@property的环境里覆盖注册,并提供一套无需动画的静态样式。例如数字滚动,如果不支持,那至少显示最终结果,不要让人等一个不会跳动的0。

注册属性时要注意的几个坑

忍不住想提醒几件事,都是我自己踩过的。

  • 你在CSS中写@property时,所有声明都必须是有效的。如果语法字符串写错,比如把<number>写成了number,那整个属性会被忽略,且浏览器会告诉你Property '--xxx' registration failed
  • inherits不是填不填都行。如果你把inherits: true,那么子元素会自动继承这个变量,类似普通CSS变量。但如果你不想让子元素也跟着变化,就声明false,然后自己给需要的元素设置独立值。
  • 注册后initial-value是该变量的默认值。假如你在动画里设置了from没有设置to,那结束时会回到initial-value。
  • 一个比较隐晦的点:动画过程中,浏览器为了让变量按照数值插值,会在每一帧对变量做“周期性转换”。所以你最好不要用在对性能极其敏感的页面里大量使用。几十个元素的数字动画还好,但如果你给几千个DOM同时做渐变旋转,CPU可能会冒烟。

用JavaScript动态注册也是后路

如果你不想把所有属性都写在CSS顶层,也可以选择在JS里注册:

if ("registerProperty" in CSS) {
  CSS.registerProperty({
    name: "--progress",
    syntax: "<percentage>",
    inherits: false,
    initialValue: "0%"
  });
} else {
  // Houdini不支持时你还能做点什么
}

这里注意JS里字段是initialValue(驼峰),别写错成CSS风格的initial-value

但JS注册有一个时序问题:如果注册发生在样式计算之后,那么某些已经应用的动画不会重新触发生效。所以尽量在页面加载早期、任何样式使用它之前完成注册。

我们能拿它做什么乱七八糟的事

我自己在业余项目里用@property做过一个非常装的仪表盘:数字不断滚动,且仪表盘的圆弧扫过时,圆弧的渐变颜色也跟着变化。一个复杂的状态以前既要用canvas又要用js动画,现在依靠几个注册变量和CSS动画,大约少了一半代码。

这种能力其实属于“CSS Houdini”的一部分。Houdini不是一个可以点击安装的插件,而是一系列让开发者能打开浏览器底层样式的API总称。@property是其中最先落地、也最容易上手的一个入口。

当我第一次看到数字计数器用原生CSS动画跑起来的时候,我心里想的是:“以后还是尽量让浏览器做它更擅长的事情吧。” 虽然JavaScript动画库依然很强大,但CSS动画能做的,让它自己做好,既省了主线程,又不容易出现闪烁。

写到最后

做前端总容易陷入“实现功能就去谷歌代码”的循环,但CSS的玩法更新速度可能比你想象的快。今天这个@property三年前我根本不敢想能这样用,现在它已经能进入实际项目了。如果你还不确定哪一步开始,建议先拿数字滚动这个案例改着玩,把syntax类型换成<integer><length>都试试。当你发现原来变量也能像SVG里的animate一样流畅变化,那种打开新世界大门的感觉挺好玩的。

CSS @property 不只是变量:从数字滚动到渐变动画,让浏览器自己算过渡
收藏 (0) 打赏

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

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

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

淘吗网 css CSS @property 不只是变量:从数字滚动到渐变动画,让浏览器自己算过渡 https://www.taomawang.com/web/css/2726.html

常见问题

相关文章

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

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