你可能见过这种效果:数字从 0 滚到 100,看起来真带劲。以前用 JS 写,或者靠 requestAnimationFrame 硬算。但今天我想试试 CSS 的新功能 —— @property。你可能听说过这个词,但八成没真正把它用在正经地方。我来演示一个纯 CSS 的数字动画,顺便把渐变过渡也搞顺滑。
先说说这个@property到底是个啥
按官方说法,它就是让你自己定义 CSS 自定义属性的类型、初始值、继承方式。用大白话说:以前 CSS 变量就是个筐,啥都能装,但浏览器不知道它是数字、颜色还是长度。现在你用 @property 告诉浏览器:“这个变量是数字,可参与过渡和动画。”然后浏览器就能对你的变量做插值计算了。
没有它之前,你想让 CSS 变量动起来,基本没戏。因为变量是字符串,浏览器不会自己算 10 到 20 的中间值。现在你说它是 <number>,好,那你给它从 10 变到 20,它就会自动计算中间的 10.5、11.8 这些值。这就是它最大的魔法。
最简单的例子:数字滚动器
我搞一个计数器,从 0 滚到 100,不用 JS,只用 CSS 动画。
@property --num {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
.counter {
counter-reset: num var(--num);
animation: count 3s ease-out forwards;
}
.counter::after {
content: counter(num);
}
@keyframes count {
to {
--num: 100;
}
}
等等,这怎么没看到数字变?因为 counter-reset 和 counter() 必须配合伪元素。但这里有一个坑:counter-reset 里的 var(--num),在动画过程中到底能不能实时变化?说实话,目前浏览器对这个的支持还是有点微妙。我测试的时候,Chrome 下没问题,但 Firefox 有点一言难尽。如果你只是想要一个平滑的数字动画,直接用 @property 配合 attr() 或者普通元素的 textContent 可能更可靠。
那换个更靠谱的思路:用 <number> 类型,然后把它赋给一个元素的 transform 或 opacity。这能实现真正的平滑插值。
让渐变背景动起来
以前要实现背景渐变的滚动,你只能欺骗自己:用背景位置变化或者双层渐变。但有了 @property,你可以直接注册一个 <color> 类型的变量,然后让颜色本身在两种颜色之间渐变。看这个:
@property --color-1 {
syntax: "<color>";
initial-value: #ff6b6b;
inherits: false;
}
@property --color-2 {
syntax: "<color>";
initial-value: #4ecdc4;
inherits: true;
}
.gradient-box {
background: linear-gradient(90deg, var(--color-1), var(--color-2));
transition: --color-1 1s, --color-2 1s;
}
.gradient-box:hover {
--color-1: #f9ca24;
--color-2: #6ab04c;
}
你在浏览器里试一下,鼠标悬停上去,颜色不是瞬间切换,而是平滑地从红变成黄,从蓝绿变成绿。这感觉很棒,但以前你要用 JS 去搞,或者用伪元素做透明过渡。现在直接定义变量过渡即可。
真正的实战:进度条圆环动画
圆环进度条是常见需求。以前用 SVG 的 stroke-dasharray 过渡,或者 Canvas。现在用 @property 配合 angular-gradient 可以搞出酷炫效果。
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.circle {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(
from var(--angle),
#16a085 0%,
#16a085 var(--percent, 50%),
#eee var(--percent, 50%),
#eee 100%
);
transition: --angle 0.3s ease, --percent 0.3s ease;
/* 简化:把 --percent 也注册下 */
}
@property --percent {
syntax: "<percentage>";
initial-value: 0%;
inherits: false;
}
这样你只需要在 JS 里改 --percent 的值,动画就会自动转。但这里的问题是 conic-gradient 中的 from var(--angle),这个角度动画是不是能实时驱动?在我测试中发现 Chrome 可以,但需要配合注册 --angle 为 <angle> 类型。
顺便说一句,@property 还可以用来做卡片翻转、数字跳动、进度条,等等。关键是你得先“注册”变量,告诉浏览器这个变量的类型。
使用注意和兼容性
目前 Chrome、Edge、Opera 都支持 @property,Safari 在 16.4 左右也支持了。Firefox 迟迟没动静,但近期已经在 flags 里出现了。如果你要用于生产,记得用 @supports (property: 1)?其实没有这个判断语法。你可以用 JS 检查是否支持 CSS.registerProperty,那个更靠谱。
还有一点,@property 必须写在 CSS 的顶层,不能在选择器里面,也不能嵌套在 media 里。它是全局定义。
注册的时候,inherits 必须显式指定,不然语法错误。别偷懒。
如果你设置 initial-value 的类型和 syntax 不匹配,对不起,整个规则会无效。
小技巧:让多个变量一起动
比如你做一个卡片,想让它看起来有“呼吸”效果。你注册一个 --scale 的 number 类型,然后在动画里从 1 变到 1.02。或者你把它用在文字阴影上,通过改变阴影距离模拟漂浮,效果很微妙。
@property --float-y {
syntax: "<length>";
initial-value: 0px;
inherits: false;
}
.float-element {
transform: translateY(var(--float-y));
animation: float 3s ease-in-out infinite alternate;
}
@keyframes float {
from {
--float-y: 0px;
}
to {
--float-y: -12px;
}
}
因为在 @keyframes 里,--float-y 被注册成了长度类型,浏览器会插入每一帧的中间值,你的元素就像被弹簧拽着一样上下浮动,丝般顺滑。
说实话,这玩意也有鸡肋的地方
你不能注册一个对象或数组类型,只能定义基础类型。而且 CSS 动画中如果变量在多个地方使用,性能不一定好,你得自己控制不要滥用。不过对于数字、颜色、角度、长度这些常见类型,它已经非常实用了。
另外,有些时候你用 transition 和 @property 配合,但遇到 display:none 切换或者元素初次渲染,可能不会出现过渡效果。解决办法是给元素加一个小的 animation 触发器。
你可能想问:那我是不是可以扔掉 JS 动画库了?
别做梦。复杂的时间线、缓动控制还是交给 JS 好。但如果你只是需要一个简单的数字变化、颜色渐变、旋转动画,CSS 完全够了。而且让 UI 层面的东西更接近“声明式”,代码可读性还高,调试也不麻烦。
拿我上面那个圆环进度条来说,你只要在控制台里跑一下 element.style.setProperty('--percent', '75%'),圆环就自己转过去了。这不比手写 raf 香?
最终的案例:一个正经的仪表盘计数
我做一个百分比显示器,圆形,中间显示数字,外圈是 conic-gradient 进度。数字部分用 counter 可能有点毛病,那我就直接用 --num 变量的数值来渲染?但 CSS 不能直接显示变量。所以用伪元素的 content 配合 counter,你只需要在 JS 里设置 --num 的整数值,然后 circle 的颜色随之变化。
@哟?
不行,还是上完整代码。简单的 HTML:
<div class="meter" style="--num: 30"></div>
CSS 里:
@property --num {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
.meter {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(#16a085 calc(var(--num) * 1%), #eee 0%);
transition: --num 0.5s cubic-bezier(0.26, 0.54, 0.32, 1);
}
.meter::after {
content: counter(num);
display: block;
text-align: center;
line-height: 100px;
font-size: 24px;
counter-reset: num var(--num);
}
但是 counter-reset 无法接受 calc?其实可以。不过动画时 counter-reset 未必能插值。在这个场景下,数字的变化可能不会很平滑,但环的过渡是平滑的。你要是想让数字也顺利滚动,还得去让 --num 作为整数变化,浏览器可以插值吗?syntax: "<integer>" 支持离散插值步骤,并不是平滑的小数。但如果你的数字变化很快,比如 1 秒内从 0 到 100,它其实是一步一步跳着走的,但人眼看起来接近平滑。想要真平滑,可以注册为 <number>,然后使用 round() 函数或者借助 --display-num 这种 hack。
有点复杂,不过基本思路有了。
最后,别把 @property 想得太神奇
它就是让你能告诉浏览器“这个自定义属性是某种类型”,仅此而已。但它确实打开了 CSS 动画的新大门。那些以前需要借助 JS 才能做的过渡,现在可以完全用 CSS 搞定。试想一下,如果你要做一个主题切换,颜色变量本来在 :root 里,你给它加上 @property 注册成颜色类型,然后过渡时间给上 0.3 秒,那换主题的时候,颜色就像渐隐渐显一样过渡,而不是瞬间切换。这不是很爽吗?
所以你可以试着把项目里的颜色变量、间距变量都注册上类型,然后放心大胆地加 transition。也许那些一直被你吐槽“硬切”的交互,一下子就丝滑起来了。好了,CSS 的魔法就到这,动手去试试吧。

