最近写几个小动效,翻来翻去发现不得不提一下 @property,这玩意现在浏览器支持度越来越好了。以前咱们做动画全靠改数字、改颜色,但遇到渐变这种“复合值”就只能干着急。现在有了 @property,可以把自定义属性变成真正可以被浏览器理解的数据类型。
我说个更直白的例子:以前你想把 background 里的角度从 0deg 动画到 360deg,或者把一种颜色过渡到另一种颜色,浏览器根本做不到。因为 var() 替换出来是一堆字符串,它不知道是角度还是颜色。但你要是用 @property 提前把变量声明成 <angle> 或者 <color>,浏览器就知道怎么去插值了。
这篇文章不带废话,直接拿一个圆环进度仪表盘来演示,看看 @property 到底能解决啥问题。
1. 先搞个最基础的圆环
圆环用 conic-gradient 来画,原理很简单:一个圆柱体背景,中间用一层圆盖住,露出外圈。比如这样:
<div class="ring"></div>
.ring {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(#6e92fe 0%, #6e92fe 25%, #eee 25%, #eee 100%);
display: flex;
align-items: center;
justify-content: center;
}
.ring::before {
content: "";
width: 160px;
height: 160px;
border-radius: 50%;
background: white;
}
这种写法很容易理解:conic-gradient 从12点方向顺时针转,前25%用一个颜色,剩下75%用灰色。一眼看上去就是一个25%进度的圆环。
如果你只是静态显示没问题,但要做成动画就麻烦了。我最初想的是把百分比换成一个变量,然后让变量自动动起来。
.ring {
--percent: 25%;
background: conic-gradient(#6e92fe 0%, #6e92fe var(--percent), #eee var(--percent), #eee 100%);
}
.ring:hover {
--percent: 80%;
}
你用 hover 改变 --percent,会发现那个颜色变化是“啪”一下跳过去的,没有过渡。这是因为 --percent 在浏览器眼里就是一个普通字符串,它不知道 50% 到 80% 之间该怎么过渡。
要是把 --percent 直接放进 transition 呢?
.ring {
--percent: 25%;
transition: --percent 0.5s;
background: conic-gradient(#6e92fe 0%, #6e92fe var(--percent), #eee var(--percent), #eee 100%);
}
不好意思,没用。因为在注册之前,CSS 自定义属性就是不参与过渡的。哪怕你写 transition: all,它照样跳变。
2. @property 怎么解决
@property 就是给自定义属性加个“身份证”。你明确告诉浏览器:这个变量是一个 <percentage>,或者 <color>,或者 <angle>。浏览器知道了类型,自然就会算插值了。
我们要把 --percent 注册成 <percentage>,代码如下:
@property --percent {
syntax: "<percentage>";
inherits: false;
initial-value: 25%;
}
注意 syntax 的写法,要把类型尖括号放在引号里。这里表示这个属性只能填百分比值。如果写 0.25 这种小数,不符合语法,它就会退回初始值。
现在再把这个变量和动画结合起来。
2.1 基于 hover 的过渡
.ring {
--percent: 25%;
transition: --percent 0.6s;
background: conic-gradient(#6e92fe 0%, #6e92fe var(--percent), #eee var(--percent), #eee 100%);
}
.ring:hover {
--percent: 90%;
}
这样当鼠标悬停时,圆环就会顺滑地转到90%!没有 @property 这根本不可能实现。
2.2 做成循环动画
如果想让它像加载动画一样一直转呢?直接注册一个 --angle 变量,配合 @keyframes 来驱动。
HTML 还是那个圆环,但我们把背景写成可变的:
.ring {
--angle: 0deg;
background: conic-gradient(
from var(--angle),
#ff6b9d 0%,
#6e92fe 30%,
#eee 30%,
#eee 100%
);
animation: rotate-ring 2s linear infinite;
}
@keyframes rotate-ring {
from { --angle: 0deg; }
to { --angle: 360deg; }
}
同样,必须在上面加一条规则,告诉浏览器 --angle 是一个角度:
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
有了这玩意儿,浏览器才会一帧一帧地计算角度的中间值。如果你不写 @property,整个动画就不会生效,--angle 会直接从 0deg 跳到 360deg。
3. 完善一个渐变仪表盘
下面我们把刚才的技术串起来,做一个有点设计感的仪表盘。这个仪表盘由两个环组成:一个底环,一个动态高亮环。我还特意做了一点渐变,看起来更炫一点。
第一步:基础结构
<div class="gauge">
<div class="gauge__track"></div>
</div>
第二步:注册变量
@property --p {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
第三步:外环样式
.gauge {
width: 240px;
height: 240px;
border-radius: 50%;
background: conic-gradient(
from 180deg,
#f64f59 0%,
#c471ed var(--p),
#e8e8e8 var(--p),
#e8e8e8 100%
);
position: relative;
transition: --p 0.8s;
}
.gauge__track {
position: absolute;
inset: 18px;
border-radius: 50%;
background: #1f1f1f;
}
我把 from 180deg 把起点放在左半圆,这样更有仪表盘的感觉。进度从红色渐变到紫色。动态部分用 --p 控制。
第四步:让变量可变
现在加一点交互,比如鼠标点击时进度到 75%。
.gauge {
cursor: pointer;
}
.gauge:active {
--p: 75%;
}
就这两行,配合前面的 transition: --p 0.8s,点击的时候就会看到进度环慢慢涨上去,而且颜色也会从 0% 的地方开始平滑过渡。
为了让第一次打开页面也能演示,我加了一个 animation:
@keyframes load-gauge {
from { --p: 0%; }
to { --p: 65%; }
}
.gauge {
animation: load-gauge 2s ease-out forwards;
}
这个动画在页面加载时执行一遍,从一个空环变成65%的状态,非常丝滑。
4. 又一个真实用处:动画颜色
@property 不只能处理角度和百分比,还能处理颜色。比如你注册一个 --main-color 类型是 <color>,然后你就可以直接在 transition 里让颜色有个渐变过渡。以前用 filter 或 box-shadow 还得绕来绕去,现在直接改变量就行。
给你个简单例子:
@property --color-a {
syntax: "<color>";
inherits: false;
initial-value: #4f79ff;
}
@keyframes color-pulse {
0% { --color-a: #4f79ff; }
100% { --color-a: #ff4f4f; }
}
.btn {
background: var(--color-a);
animation: color-pulse 1s infinite alternate;
}
这个按钮的背景色会一直在蓝和红之间来回变化。要是没有 @property,就不能直接对 background 里的 var 值做插值。
5. 注意几个小坑
第一,@property 规则必须放在全局或者父级里吗? 不一定,它和常用的 @keyframes 一样,可以在任意 CSS 里写,但通常放在顶层,方便统一管理。它不属于任何选择器,就是管一个变量定义。
第二,inherits 字段到底啥意思? 其实就是说这个变量是否被子元素继承。如果你写 inherits: true,子元素可以直接用这个变量,并且如果子元素没有重新赋值,就会用父元素的值。如果你写 false,子元素不能继承,除非自己定义。我一般做组件都用 false,因为我只想这个组件内部用它。
第三,syntax 语法必须严格。 比如你要注册一个类型是颜色加透明度,就得写 "<color>",不能写 "<color>" <percentage> 这种,除非你使用组合语法,像 "<color> || <percentage>",但是浏览器支持还不完全。所以我平时就用最基础的类型:<length>, <percentage>, <angle>, <color>, <number>。
第四,兼容性怎么看? 目前 Chrome、Edge、Opera 早就支持了,Firefox 也在今年(2024)版本里默认支持了。Safari 从16.4开始支持 @property。所以现在在移动端 WebView 里用也没什么问题。要是实在需要兼容老 Safari,可以这样写:底色的初始值直接写在 background 里,再用 @supports (background: paint(worklet)) 之类的功能检测。当然最好是直接用 @supports (--custom: 1)?其实也可以用 @supports (background: var(--p)) 来判断。
最实用的降级方案:老老实实写一个静态背景作为 fallback,再在支持 @property 的浏览器里覆盖。
.gauge {
background: conic-gradient(#f64f59 65%, #e8e8e8 0);
}
@supports (--p: 0%) {
.gauge {
background: conic-gradient(
#f64f59 0%,
#c471ed var(--p),
#e8e8e8 var(--p),
#e8e8e8 100%
);
}
}
这样高级浏览器看动态,老年浏览器也能看到一张静态的图。
6. 最后说点实际的
我知道很多人看到新特性第一反应就是“又有新东西要学了,真累”。但 @property 这玩意儿,说白了就是让你之前绕了很多路的需求变得更加直接。以前想做圆环动画,要么用 SVG 的 stroke-dasharray,要么用一堆 JS 去改样式,现在用两行 CSS 就能搞定,它不香吗?
再加上现在浏览器迭代速度飞快,大部分用户都已经跑在支持 @property 的版本上了。所以放心地在个人项目、业务系统里用起来吧,至少做个加载动画不用再求 PM 换一个 GIF 了。
真到了不太行的时候,补一个 fallback 也只是几行代码的事。不亏。
对了,文章里这些代码我都跑过,放心抄作业。要是有啥奇怪的兼容性差异,欢迎在评论区一起讨论。
行了,这次就唠到这,下次我再搞点别的 CSS 新特性实战,等着。

