你肯定在CSS里用过自定义属性,也就是以--开头的变量。比如:
:root {
--main-color: #ff6b6b;
}
.button {
background-color: var(--main-color);
}
这种变量用着挺方便,但它有一个让人头疼的缺点:没法单独对自定义属性做过渡或动画。因为浏览器不知道这个变量到底是颜色、长度还是数字。你写transition: --main-color 0.3s,它根本不理你。
但今天要聊的@property,可以说是“官方开挂”了。它可以让你自己注册一个自定义属性,明确告诉浏览器:这个属性是颜色、是百分比还是整数。这样一来,浏览器就能理解它,然后就可以对这个变量本身做过渡和动画了。
一个让我抓狂的场景
去年做进度条的时候,我想让进度数字带动画走:从0%变到80%。我本来以为可以用CSS变量+transition搞定,于是写了类似这样的代码:
<div class="progress" style="--percent: 80%">
<span class="progress-text"></span>
</div>
.progress {
--percent: 0%;
transition: --percent 1s ease-out;
}
.progress:hover {
--percent: 80%;
}
结果完全不按套路出牌,--percent瞬间从0跳到了80%,根本没有过渡动画。查了一下,才发现自定义属性默认是无类型的,浏览器没法判断它怎么插值。
用 @property 注册一个“可过渡”的变量
@property的作用就是给自定义属性声明类型。我把刚才的--percent注册成一个“百分比”类型的属性:
@property --percent {
syntax: '<percentage>';
inherits: false;
initial-value: 0%;
}
这里三个关键属性:
syntax:定义属性类型,这里是百分比<percentage>。inherits:是否继承父元素的值,通常设成false。initial-value:初始值,必须符合syntax。
注册完成以后,--percent在浏览器眼里就不再是一堆没意义的字符,而是一个真正可以计算的百分比。再跑一遍刚才的代码,进度条就能平滑过渡了。
实战:做一个带动画的圆环进度
光说没用,我直接给你一个完整的案例。用@property做一个鼠标悬停时进度从0%到90%的圆环,数字也跟着走。
先写HTML:
<div class="circle" style="--progress: 90%">
<span class="circle-num"></span>
</div>
然后是注册变量和样式。看好了,这里用了@property,没加任何JS,数字和圆环都能动。
@property --progress {
syntax: '<percentage>';
inherits: false;
initial-value: 0%;
}
.circle {
width: 150px;
height: 150px;
border-radius: 50%;
background: conic-gradient(
#4f9cf9 var(--progress),
#e5e7eb var(--progress)
);
display: flex;
align-items: center;
justify-content: center;
transition: --progress 0.8s ease-out;
position: relative;
}
.circle::after {
content: "";
position: absolute;
inset: 12px;
background: white;
border-radius: 50%;
}
.circle-num {
z-index: 1;
font-size: 28px;
color: #333;
}
.circle:hover {
--progress: 30%; /* 鼠标移上去会从90%过渡到30% */
}
这里conic-gradient用百分比决定色标位置,如果--progress不是可插值的类型,鼠标悬停时进度会直接跳变。现在注册成百分比后,就能看到圆环的填充区域平滑地缩小或扩大,非常神奇。
但要显示数字从0变到90,单单靠CSS没法把内容写进伪元素。我在这里用了一个小技巧:在HTML里塞一个span.circle-num,然后通过CSS计数器?其实计数器也没法直接提取格式化百分比。所以我选择在style属性里放一个--progress,再用JS动态设置文本内容。毕竟本篇文章重点是@property,数字部分不用太较真。你完全可以用background技巧做纯CSS数字展示,但大多场景下配合JS读取变量更简单。
更实用:自定义属性也能做颜色过渡
再介绍一个能用的场景:贝塞尔曲线加载动画。比如一个按钮,背景色从蓝色渐变到紫色。之前你只能通过background属性过渡,但要是想单独把主题色变量做成动画,就得靠@property。
先注册一个颜色变量:
@property --theme-color {
syntax: '<color>';
inherits: false;
initial-value: #ff6b6b;
}
然后:hover改成另一个颜色:
.btn {
background-color: var(--theme-color);
transition: --theme-color 0.4s ease;
}
.btn:hover {
--theme-color: #4f9cf9;
}
以前这种写法是没用的,因为--theme-color不分类型。现在注册成<color>后,鼠标移上去,背景色会从偏暖的红色平滑过渡到冷蓝。在主题切换、动态换肤这类场景里特别有用。
还可以玩数字:自定义计数器动画
如果注册一个<integer>类型,你甚至可以让数字滚动起来。比如通知角标从1变到99:
@property --count {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
然后利用CSS计数器?但计数器的值没法直接显示自定义属性。这个需要配合@property和伪元素的content吗?
很遗憾,目前content还不能直接使用任意的自定义属性数值。除非你写的值是字符串类型syntax: '<custom-ident>'。不过<integer>最常被用在animation的steps()里,也能控制一些数值参与计算。更常见的玩法是结合counter和@property?其实没法完美展示。所以数字动画目前我还是选择用JS,CSS变量只负责驱动样式。
回到圆环:让百分比数字一起动
我发现很多人喜欢问“怎么让数字跟着动画变”。这里我给出一个方案,用@property配合JS的requestAnimationFrame或transitionend。但既然核心是CSS,我不展开写JS,简单说下思路:监听--progress变化,并不现实。所以最稳妥的做法是用JS读取当前值然后写到textContent里。
但你也可以用CSS的attr()?不行。目前还没法把数值变量直接渲染到伪元素content里。所以不要在纯CSS数字上死磕,那个方向不是@property的本职。
浏览器兼容性,现在是怎样?
@property的出现比大家预期的早,Chrome 85就开始支持了,Edge也一样。Firefox 128在2024年也终于默认支持。Safari 16.4开始支持,也就是说,现在主流浏览器都可以放心用。唯一的短板是老的WebKit版本,但早就不在考虑范围了。
如果你的项目需要兼容旧浏览器,可以先把普通变量作为fallback:
.progress {
--percent: 0%;
background: conic-gradient(#ccc 0%, #eee 100%);
transition: none;
}
然后在支持@property的浏览器里再注册真正的属性,让动画发生。因为不支持@property时,浏览器不认识这个规则,会忽略掉,而普通变量还是能用的。
踩坑提示
有几个坑我一开始也没注意:
@property规则必须写在顶层或者CSSOM里,不能嵌套在媒体查询里。initial-value必须符合syntax,比如注册<integer>,初始值不能写0.5。- 注册的变量如果是
<length>,那赋值时必须带单位,比如10px。不带单位的数字会被视为无效。 - 过渡动画和
transition搭配时,变量名不能加--前缀以外的内容?不对,就是直接写--progress就行。
另外,syntax还支持很多类型:<angle>、<time>、<resolution>、<color>、<percentage>、<length>、<number>等等。还有复杂的组合比如''字符串类型,我暂时没觉得用处很大。
一个进阶技巧:利用@property做贝塞尔曲线动画
既然变量已经能过渡,那我们可以用它做更高级的事情。比如做一个数字跳动效果:
@property --num {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
然后在动画里从0变到10:
@keyframes count-up {
from { --num: 0; }
to { --num: 10; }
}
.number {
animation: count-up 1s forwards;
}
虽然没法直接把--num显示出来,但你可以用calc(var(--num) * 10%)去驱动一个进度条。也就是说,--num变成了一个可以参与运算的数字,这跟以前只能整块替换完全不一样了。
再举个例子,把一个元素翻转角度从0度变成180度,以前只能使用transform的变量,但那个变量没法插值。现在:
@property --angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.box {
transform: rotate(var(--angle));
transition: --angle 0.5s;
}
.box:hover {
--angle: 180deg;
}
这个效果以前必须写一大段复杂的calc或者用JS,现在直接定义变量类型搞定。
总结:@property到底改变了什么
简单说,@property让CSS自定义属性从“字符串替换”变成了“可计算的动态变量”。它最大的作用是让你的变量能参与到过渡、动画、渐变插值这些需要数值计算的过程中去。这种能力在主题系统、交互动效、loading动画里都特别有用。
我现在的感悟是,CSS越来越像一个正经的编程语言了。以前靠黑魔法或者JS辅助实现的动画,现在几行原生CSS就能写出来。如果你还没试过@property,建议打开浏览器控制台,把文章里圆环进度那一段复制过去跑一下。你会看到那个百分比真的在平滑移动,那种感觉很奇妙。
以后别再只把CSS变量当纯文本了,它们也可以有自己的“性格”和“动态”。

