transition 写在 CSS 变量上为什么不生效?给变量补张「类型身份证」

把滚动进度写进 CSS 变量去驱动别的属性,动画直接罢工。不是变量坏了------是它压根没资格参与插值。今天看一下 @property

0. 两行代码,动画罢工

css 复制代码
.bar {
  width: calc(var(--p) * 1%);
  background: #16a34a;
  transition: --p 1s linear;  /* 我就是要对变量做过渡,怎么了? */
}
js 复制代码
el.style.setProperty('--p', '0');
getComputedStyle(el).width;      // 触发一次样式计算(注意:这并不能真正锁住起始值)
el.style.setProperty('--p', '100');

transition 写一个属性名而已,--p 也是属性名,语法完全合法,编辑器也不报错。然后屏幕就出现了进度条瞬间满格,一帧过渡都没有。 demo测试:

css 复制代码
[T1] --q 未注册 变化节点 1 个: [[0, "100"]]
     判定: 秒跳(transition 无效,值瞬时到位)

这个轨迹变化节点只有 1 个,而且第一帧就是 "100" ------连「从 0 显示一帧」的机会都没给,.setProperty() 的那一刻直接跳到终点。这并不是 transition 效果不好,是压根没工作。 顺带一提:getComputedStyle().width 返回的是 resolved 值,对未注册变量驱动的属性,这个动作并不能「锁住起始值」,实际当中需要注意。

1. @property,注册变量

css 复制代码
@property --p {
  syntax: '<number>';   /* 类型:这是个数 */
  inherits: false;      /* 继承:不往下传 */
  initial-value: 0;     /* 初始值:没设置时兜底 */
}

注册之后,--p 就从「字符串占位符」变成了有类型、有初始值、有继承语义的真属性 。同样的 transition: --p 1s linear,现在开工了:

less 复制代码
[T2] --p <number> 变化节点 61 个: [[0, "0"], [17, "1.67"], [33, "3.34"],
    [50, "5"], [67, "6.66"], [83, "8.34"], ...(共 61 个节点,每帧稳定步进约
    +1.67,精确线性插值到 [1000, "100"],完整轨迹见 验证输出.txt)
     判定: 逐帧插值 ✓

60fps 采样一秒拿到 61 个节点,步进均匀------这不是看起来动了,是逐帧可查的插值轨迹。可以在浏览器里面运行一下试试看效果,看多少遍都不如跑一遍。

支持度(截至发稿):Chrome 85+、Safari 16.4+、Firefox 128+,Baseline 2024 全绿,放心用。

但检测的时候别用 CSS.supports

python 复制代码
[ENV] CSSPropertyRule 接口: True | CSS.supports("@property --x: 0"): False
      | CSS.registerProperty: True

CSSPropertyRule 接口存在、注册功能一切正常,CSS.supports("@property --x: 0") 却返回 False,那是因为 CSS.supports 只认属性声明,不认识 at-rule。

要检测的话就查 typeof CSSPropertyRule !== 'undefined'

2. 变量为什么天生不能插值

普通 CSS 变量只是一个文本替换var(--p) 在解析阶段被替换成字面的 token 串,然后浏览器才去理解这条声明。

也就是说在插值器眼里,--p 的值可能是 100、可能是 red、可能是 hello world它是一段没有类型的字符串,而字符串没有中间值

从 0 到 100 的数字插值需要知道这是数,中间态是 0.5、1.5、2.5;从红到蓝的颜色插值需要知道这是颜色,中间态是紫色。不知道类型,插值器只能罢工,transition 规范干脆把未注册的自定义属性标记为离散动画。 @property 的本质就是把类型声明补上:syntax 告诉浏览器值长什么样,插值器立刻恢复工作。它属于 CSS Houdini 的 Properties and Values API------JS 侧还有个等价的 CSS.registerProperty(),但 CSS 里能写的就别动 JS,@property 在样式表加载时就生效,不会闪一下未注册成功时候的状态,最厉害的是渐变的色标也能逐帧插。

css 复制代码
@property --c1 { syntax: '<color>'; inherits: false; initial-value: #ef4444; }
@property --c2 { syntax: '<color>'; inherits: false; initial-value: #3b82f6; }
.gbox {
  background: linear-gradient(90deg, var(--c1), var(--c2));
  animation: hueShift 2s linear infinite alternate;
}
@keyframes hueShift {
  from { --c1: #ef4444; --c2: #3b82f6; }
  to   { --c1: #facc15; --c2: #16a34a; }
}

1 秒采样,background-image 出现 62 种不同插值:

css 复制代码
[T3] backgroundImage 不同插值 62 种(1 秒内)
[T3] 首帧: linear-gradient(90deg, rgb(247, 165, 34), rgb(32, 154, 123))
[T3] 末帧: linear-gradient(90deg, rgb(248, 173, 32), rgb(30, 156, 113))
     判定: 颜色逐帧插值 ✓

(动画是无限循环,采样时已在途中,所以首帧不是 from 色------62 种不同值本身就是逐帧在变的铁证。)

当然,传统做法用 keyframes 直接动画 background-position 或 opacity 也能近似部分渐变效果,@property 并非唯一解;但像 conic-gradient 的角度旋转这类,纯 CSS 就只有 @property 能做到了。

顺便把上篇遗留的内容也一起说了:滚动进度写进注册过的 --p,就能继续驱动任何属性------calc(var(--p) * 1px) 平移、opacity: calc(var(--p) / 100) 淡入,全跟手。这个场景里 inherits 建议设 true:祖先元素设置一次,所有后代元素都能消费,不用逐个赋值。

3. 误区

3.1 注册了 ≠ 能插值

syntax: '*' 是万能语法,什么值都能装。装完会发现照样秒跳

css 复制代码
[T4] --u(syntax:*)变化节点 1 个: [[0, "100"]]
     判定: 照样秒跳------注册了 ≠ 能插值,类型才是门票

'*' 的值是任意 token 串,能兜底初始值、能控制继承,但插值还是免谈。想动画,就老实声明具体类型。

3.2 initial-value 写错,整条规则都没用

syntax 不是 '*' 时,initial-value 必填,而且必须能被该类型解析。我故意写了条缺 initial-value--bad,然后翻了一下 CSSOM:

less 复制代码
[T5] CSSOM 中 CSSPropertyRule 列表: ["--p", "--u", "--c1", "--c2", "--reg", "--inh", "--noinh"]
[T5] --bad 在 CSSOM 中? False

七个合法注册全在,--bad 找不到,也没有报错和警告、控制台干净得像什么都没发生,规则整条被丢弃。

4. 自带能力

4.1 没值也有值

未注册变量没设置就是空串;注册过的变量没设置,读出来是 initial-value计算值

csharp 复制代码
[T6] 未设置时读 --reg(注册 <color> initial teal): "rgb(0, 128, 128)"
[T6] 未设置时读 --never-set(从未声明)         : ""

两个细节:

  1. 兜底真的存在(var() 可以少写一层 fallback);
  2. teal 读出来是 rgb(0, 128, 128)注册值会按类型规范化,不是原样存取的字符串。做主题切换时这是个省心设计,但也别拿它当原始字符串比对。

4.2 inherits 两态,行为对得上直觉

css 复制代码
@property --inh   { syntax: '<number>'; inherits: true;  initial-value: -1; }
@property --noinh { syntax: '<number>'; inherits: false; initial-value: -1; }

父元素把两个都设成 7,子元素读:

csharp 复制代码
[T7] 子元素读 --inh(inherits:true)  = "7"
[T7] 子元素读 --noinh(inherits:false)= "-1"

false 的子元素拿到的不是空串,是 initial-value -1,不继承,有兜底值。 最后提醒一句性能:注册变量被大量元素引用时,逐帧插值会连带触发所有依赖属性的样式计算,重绘成本可能高于直接动画属性本身。小范围使用没问题,全页面铺开前先掂量一下。

面试题

Q1:为什么未注册的 CSS 变量不能参与 transition/animation? 普通自定义属性的值替换发生在解析阶段,插值器拿到的是无类型的 token 串------没有「中间值」概念,规范将其归为离散动画(直接跳变)。@property 通过 syntax 补上类型声明后,浏览器才能计算中间态,逐帧插值。这也是渐变色动画(色标是 <color> 类型)能纯 CSS 实现的前提。 Q2:syntax: '*'<number> 注册的变量,行为差异是什么? '*' 是万能语法:接受任意 token 串、可省略 initial-value,但值不可插值(等于有兜底的未注册变量)。<number> 等具体类型:initial-value 必填且须匹配类型、非法值声明被丢弃,但 transition/animation/keyframes 全部可用。选择原则:要动画就声明具体类型,只做兜底和继承控制用 '*'Q3:@property 注册失败(如缺 initial-value)会怎样?如何排查? 整条规则被静默丢弃:CSSOM 中不出现对应的 CSSPropertyRule,变量退回未注册行为(空值、不插值、var() 走 fallback),全程无报错。排查方法:遍历 document.styleSheetscssRulesCSSPropertyRule 实例;或读一个未显式设值的元素,注册成功返回 initial-value 计算值,失败返回空串。注意 CSS.supports('@property --x: 0') 返回 False 不代表不支持------它只认属性声明。

参考

  • MDN:@property、CSS Properties and Values API
  • Chrome for Developers:@property(Bramus 系列)
  • Baseline 2024(Chrome 85+ / Safari 16.4+ / Firefox 128+,截至发稿)

demo 也已经整理好了,公众号「程序员蜡笔熊」回复「property」拿走。

预告 :变量注册的时候既然能存值,那么能不能存逻辑呢?CSS 开始长出自己的函数(@function),传参、复用、按条件返回值,结束 calc() 拼参数拼到吐。下篇看看它到底能干啥、还差啥。

期待您的评论和建议~

相关推荐
YHL1 小时前
🧠 Agent 记忆进阶:总结压缩与向量检索 —— 从截断到 Milvus 长期记忆
前端·人工智能
用户921080262861 小时前
前端 Vue 专栏 05:从 Object.defineProperty 到 Proxy,彻底理解 Vue 响应式原理
前端
Fluxart.ai1 小时前
GPT Image 2.5 有使用次数限制吗?额度、并发、频率限制说明
服务器·前端·gpt
程序员海军2 小时前
AI 越来越强,为什么打工人反而越来越累、越来越内耗了?
前端·程序员·aigc
晴天162 小时前
VSCodium 项目介绍
前端·javascript·vscode
世岩清上2 小时前
展厅数字内容同质化严重,怎样打造专属叙事风格?
大数据·前端·javascript·人工智能·html·音视频·展厅改造
晴天162 小时前
前端动画体系全解析:类型、实现方案与工程选型最佳实践
前端
持敬chijing2 小时前
Web前后端开发-JavaScript 入门教程
开发语言·前端·javascript
李高钢2 小时前
Python Django 框架入门:从零搭建你的第一个 Web 应用
前端·python·django