var() 的解析、陷阱与妙用
关键心智模型:自定义属性是一个代换值 (substitution value),不是普通属性那样「写进去就解析成具体的值」。浏览器只检查它是不是合法的 token 流(几乎什么都收),把这串 token 原样存着;直到某个 var() 在 computed value time 把这串 token 贴回到目标属性里,才真正去解析。
1 · var() 的求值过程
1.1 · fallback 的触发条件
var(--size, 80px) 的第二个参数是 fallback。但它的触发条件很窄:仅当 --size 没被定义,或被显式设成 initial(自定义属性的 guaranteed-invalid value)时才用 fallback。只要 --size 不是 guaranteed-invalid,fallback
就不会出场(哪怕它的值为空)——哪怕那内容代换后是非法的(那是 IACVT 一节(§3)的事)。点下面四个值,看同一句 width: var(--size, 80px) 取到多少:
反直觉点:--size: initial 和「完全不定义」效果一样——都触发 fallback。因为把自定义属性设成 initial 不是「设成某个初始值」,而是把它打回 guaranteed-invalid value(一个「保证非法」的特殊空值),var() 看见它就当作没定义,转而吃 fallback。空格 hack 一节(§6)正是玩这一点。
fallback 是「第一个逗号之后的全部」:var() 只把第一个逗号当分隔符,其后即便还有逗号也整段算作 fallback。所以 var(--family, "PingFang SC", sans-serif) 的 fallback 是 "PingFang SC", sans-serif 一整串(对
font-family 这类本就接受逗号列表的属性正合适);若误以为其中有两个 fallback 依次尝试,就理解错了。特例:var(--x, )(逗号后留空)语法合法,表示 fallback 是空 token 流——和 空格 hack(§6)里用到的「空值」是同一类东西。
1.2 · :root 里被冻住的是代换而非算术
自定义属性会继承,而且它继承下来的是已经代换完的 computed 值。所以在 :root 写 --double: calc(var(--base) * 2) 时,var(--base) 在 :root 当场被代换成 10px,往下传的是 calc(10px * 2) 这串
token——被冻住的是代换,不是算术。未注册的自定义属性在 computed value time 只做 var() 代换、不求值 calc():实测 getComputedStyle(:root).getPropertyValue("--double") 读回的正是 "calc(10px * 2)"。子元素后来把
--base 改大,也带不动那个早已代换定的 --double。若想让它随子元素变化,就不要在 :root 提前计算,把 calc 留到使用它的那一层。
1.3 · 循环引用的暴露时机
自定义属性可以互相引用,--a: var(--b) 在流程图第 1 格「收集声明」里完全合法——它不过是一串合法 token,浏览器并不在这一步追查 --b 是谁。可一旦引用链兜回自己(--a → --b → --a)形成 dependency cycle,问题要拖到第 4 格 computed value time
才暴露:浏览器展开 var() 时检测到环,按规范把环里涉及的每一个自定义属性一起判为 invalid,统统打回 guaranteed-invalid value。后果正好复用前两节的规则——取它的 var() 若带 fallback 就走 fallback(等同未定义);若没有 fallback、或代换进普通属性后非法,则该属性
IACVT(§3)。点下面切换「成环 / 断开」,看同一句 width: var(--a, 80px) 取到多少:
和 IACVT 区分:环不是「语法错误」——--a: var(--b) 照样能过第 1 格、照样能赢下 cascade,失败和 IACVT(§3)一样都发生在代换那一刻,而非解析期。差别在于:IACVT 是「代换出的值对目标属性非法」,环则是「代换根本无从下手——引用兜回了自己」,于是环里的变量被集体判 invalid。
小结:自定义属性 =「先把原始 token 存着,等 var() 在 computed 阶段才代换并解析」。把握这条,fallback 为何那样触发、为什么 :root 的 calc 会冻结、循环引用为何要到 computed 阶段才崩,就都能解释清楚。!important 一节(§2)会接着看流程图第 2 格「cascade 决胜」里
!important 的反直觉行为。
2 · !important 作用在声明而非值上
自定义属性几乎什么字符都收——--whats-up: (👍ᴗ_ᴗ)👍 都是合法的值。正因为太宽松,很容易以为 --color: red !important 里的 !important 也被当成值的一部分存了进去。并不是。!important 是声明的旗标,在解析时就被剥离,真正存进变量的值只有 red。
旗标和值分家,带来两个反直觉的结论,正是本节要实测的:
- 在自定义属性自身的 cascade 里,带
!important的声明压过更高 specificity 的声明——这一步就是 var() 一节(§1)流程图的第 2 格(cascade 决胜)。 - 但这个 important 不会顺着
var()传递给「用到它」的那条声明——一个常被忽略、规范却写得很清楚的细节。
2.1 · 旗标的剥离
给真实元素注入 --color: red !important,再用 getComputedStyle(el).getPropertyValue('--color') 读回来——看它到底存了什么:
两种写法存进去的值完全一样,都是 red。唯一的区别是「这条声明重不重要」——那是声明级的旗标,根本不进值里。所以顶层的 !important 无法写进自定义属性的值,它总被解析器当旗标剥离。包在块里则不受限:实测 --bang: (!important) 读回的正是
"(!important)"。
2.2 · cascade 中 !important 与 specificity 的次序
这是 Frontend Masters 那篇的核心例子。低 specificity 的元素选择器 p { --color: red !important };紧挨着用变量的 .greeting(specificity 更高)设 --color: blue。谁赢?切换 !important 看 color: var(--color) 的真实结果:
关键:specificity 只在「都不带 !important」时才说了算。一旦低 specificity 那条挂上 !important,它就反超。注意 important 在此决定的是「变量取哪个值」,与 color 这条声明是否重要无关——那是下面「important 传不过 var()」一节讨论的内容。
2.3 · 重要性不随 var() 传递
「cascade」一节里 --color 靠 !important 赢下了「变量的 cascade」,稳定解析成 red。但用它的 color: var(--color) 这条声明本身重不重要,跟变量的 important 毫无关系——它只看自己写没写 !important。所以下面再来一条更高 specificity 的普通
color: green,就能把它压下去:
心智模型:重要性分两层,各自独立,从不互相传递。第一层是 --x: … 这条声明——它的 important 决定变量最终持有哪个值;第二层是 prop: var(--x) 这条声明——它的 important 决定这条 prop 在同属性里赢不赢。变量是 important 的,绝不等于「凡是用了它的声明都变 important」。
3 · IACVT 与代换后才暴露的非法
IACVT = Invalid At Computed Value Time(在「计算值」这一步才变非法)。它与常见的「写错了被忽略」是两种失败模式,差别全在什么时候失败:
- 解析期报错——
color: 20px一进门就因类型不符被丢弃,这条声明当没写过,自然回退到上一条合法声明。 - IACVT——
color: var(--bad)语法完全合法,顺利赢下 cascade(把同属性的其他声明都挤掉了);直到 computed 阶段把--bad代换成20px,才发现对color非法。此时已经回不去上一条了——整条声明变成unset。
3.1 · 两种写法的不同结局
父元素 color: teal;子元素都先写 color: blue,再用两种方式叠一个「非法的颜色」。切换看子元素 getComputedStyle().color 的真实结果:
这是最容易误判的一点:直觉上以为 var() 失败会「优雅降级」回退到上一条 color: blue——实际不会。它早在 cascade 阶段就把 blue 挤掉了,等代换失败时只剩 unset 一条路径。所以结果既不是 blue、也不是黑,而是从父元素继承来的 teal。
3.2 · unset 的落点取决于可继承性
IACVT 把声明变成 unset,而 unset 是「见机行事」:可继承属性(如 color)→ 同 inherit,取父值;不可继承属性(如 background-color)→ 同 initial,回属性初始值(transparent)。左右两块用同一个非法变量
--bad: 20px,看落点不同:
3.3 · shorthand 的连带作废
IACVT 作废的是整条声明。若这条声明是 font / margin / background 这类 shorthand,它展开的所有 longhand 会一起 unset——哪怕只有一个子值来自非法的 var(),那些写死的、本来没问题的子值也跟着陪葬。(对比 §3 开头的两条失败模式:字面写错的 shorthand
在解析期就整条被丢,那是另一回事;本节的 shorthand 含 var(),语法合法、赢下 cascade,失败发生在代换之后。)下面两块都想要 italic 700 22px,区别只在 font-family 是写进 font shorthand、还是拆成独立 longhand。把
--family 切成非法值,看谁全垮、谁只丢字体:
结论:给 shorthand 喂一个可能失败的 var(),押上的是整条 shorthand。想缩小爆炸半径,就把易错的那个值单独写成 longhand——失败时只有它一个 IACVT 回退,其余写死的子值毫发无伤。
3.4 · 现代 CSS 里的隐蔽形态
这个问题在「用 var() 给新特性做回退」时最隐蔽。下面想用 5cqi(容器查询单位)做流体字号、给老浏览器留 var(... , 1rem + 1.5vw) 当 fallback。问题是:在支持 var() 但不支持 cqi 的浏览器里,第二条声明语法合法、赢下 cascade,代换后
5cqi 非法 → 整个 clamp() IACVT → font-size 走 unset。font-size 是可继承属性,unset 即 inherit,于是它落到父元素的字号而非 medium:实测父级 40px 时标题也得 40px,标题层级直接塌掉,fallback 根本没机会上场。
注意区分:var() 的 fallback 只处理「变量未定义」,无法处理「代换后的值对这个属性非法」。要应对「浏览器不识别这个值 / 单位」,需要用 @supports。
4 · @property 与类型化变量
前面 var()(§1)与 IACVT(§3)两节里的变量都是「未注册的」(unregistered):无类型、无 initial-value 描述符(初始值是 guaranteed-invalid)、强制继承,且只能离散插值。@property
给变量补上三件元信息——syntax(类型)、inherits(是否继承)、initial-value(初始值),它就「升级」成 registered custom property,行为更可控:
4.1 · 类型检查与非法赋值的落点
最大的不同:registered 变量有类型校验。给它一个不符合 syntax 的值,这次赋值整个无效,同样是 IACVT 走 unset:inherits: false 时落回 initial-value,inherits: true 时先取父值。本页三处演示都用 inherits: false,故都落回
initial-value;换成 inherits: true 时,父级 teal、子级写入非法的 20px,实测子级算出的仍是 teal 而非 initial-value 的 gold。而 unregistered 变量来者不拒,非法值照存,等到 var() 被使用时才以 IACVT
暴露问题(回 unset)。两块都做 background: var(--tint),给同样的值,看结果:
所以 @property 给变量提供了一道保护:即便有人传错值,组件也只会退到预设的 initial-value(一个可控的安全色),而不是整块透明或继承一个意外的值。这比未注册变量的 IACVT 行为可预测得多。
4.2 · 注册之后的 fallback
还有一个容易忽略的连锁反应。fallback(§1)只在变量是 guaranteed-invalid(未定义或显式赋 initial)时才上场;可一旦用 @property 注册并给了 initial-value,这个变量就再也回不到 guaranteed-invalid——没赋值时它是 initial-value,赋 initial 也只是退回
initial-value。于是 var(--c, fallback) 里的 fallback 形同虚设,真正起回退作用的是 initial-value。两块都写 background: var(--c, hotpink),左边的 --c 注册过(<color>,initial-value: gold)、右边没注册,切换
--c 的取值看差别:
记法:想给注册过的变量设「默认值」,改 initial-value,别再依赖 var() 的 fallback——后者只为未注册变量(可能 guaranteed-invalid)而存在。注册带来可预测性的同时,也悄悄改写了 fallback 的触发条件。
4.3 · 注册变量的动画能力
未注册变量不可插值——对它做 transition,它只会在中途跳变一次。一旦用 @property 声明了类型(如 <angle> / <color> / <number>),浏览器就知道怎么在两值间平滑插值。下面两块都 transition: --angle .8s 并旋转一个
conic-gradient,点按钮看区别:
5 · token 流代换与字符串拼接的区别
最常被误用的一点:把 var() 想成「字符串替换」。它在 computed value time(§1)做的是 token 流代换——把变量存着的那串 token 原样贴进属性值,不会重新分词,也不会和相邻字符黏成新 token。所以 var(--n)px 不会拼出 120px:120 和 px 始终是两个独立
token。先看这个最高频的「数字接单位」陷阱:
为什么不拼:源码里写 120px 是一个 dimension token;而 var(--n)px 代换后得到的是 <number 120> 紧跟 <ident px> 两个 token——代换阶段不会把它们重新合并成一个 dimension。要「数 → 长度」,得在 calc() 里做
var(--n) * 1px 这种类型乘法。
5.1 · var() 的站位限制
同样因为「token 流代换、不参与拼接」,var() 还有一组容易踩的边界——它无法用来拼属性名、嵌进选择器或 media query,也不会钻进 url() 内部去展开。下面几条都不成立,右侧给出对应正解:
一句话总结:var() 把整段 token 原样贴进属性值,既不拼字符串、不和相邻字符黏成新 token,也不能出现在属性名 / 选择器 / media query 这些非「属性值」的位置。需要「数 + 单位」用 calc(… * 1unit);需要带 url 的值就把整个 url(…) 存进变量再
var() 出来。
6 · 用一个变量开关多处值
Lea Verou 的经典技巧,把 var() 一节(§1)的 guaranteed-invalid 与 fallback 两个特性组合成一个「CSS 布尔开关」。在原生条件语法普及之前,这是用纯 CSS 做条件样式最接近的写法。它只用到两个特殊值:
--ON: initial——把变量打成 guaranteed-invalid value。被var(--x, 用这个)引用时,浏览器当它没定义,于是吃 fallback → 那处样式打开。--OFF: ;——值为空。规范明确--foo:;是合法的空值而非 guaranteed-invalid,在任何能放var()的地方都合法、且不贡献任何内容。被var(--x, fallback)引用时变量「有值」,于是跳过 fallback、代换成空,那处样式关闭。
于是一个变量 --raised 设成 var(--ON) 还是 var(--OFF),就能同时拨动好几条 var(--raised, …) 声明。点开关,看一个变量带动 box-shadow + transform + outline 三处一起亮灭:
「空格 hack」这个名字是历史遗留。早期规范不允许空值,只能塞一个空格顶上;如今 --foo:; 本身就合法,计算值是空串(实测 getComputedStyle 读回 "")。写空格的效果相同,因为计算值会把空白 trim 掉。关键不在于填了什么,而在于变量有值:有值就算「已定义」,从而优先于
fallback,这正是 --OFF 能关闭的原理。
6.1 · 追加式用法
上面的开关让整条声明在「fallback 内容」与「空」之间切。Lea 原文的形态是追加:把 var(--toggle, 额外层) 拼在一段固定值前面,开 = 多一层、关 = 只剩固定值。比如给背景可选地叠一层高光渐变:
局限(原作者也强调):这套 hack 只擅长「加 / 不加」——追加一层或整条开关。它不擅长「在 A 值和 B 值之间二选一」(比如红底 vs 白底)。真要做完整的三元条件,得等原生条件语法。这两者的进度并不同步:if() 已随 Chromium 137(2025-05)发货,实测 Chrome 151 上可用,Firefox 与 Safari 未实现;@when
与 @else 则至今无任何引擎实现(实测同版本解析失败)。在此之前,这个开关仍是覆盖面最广的纯 CSS 条件写法(据 MDN BCD,核对于 2026-08)。
相关链接
var() 与 fallback
- How Custom Property Values are Computed moderncss.dev 把「substitution value / computed value time / fallback 何时生效 / :root 固化」讲清楚的那篇。
-
CSS · var()
developer.mozilla.org
var(--x, fallback)的语法、fallback 触发条件与 guaranteed-invalid value。
!important
-
!important and CSS Custom Properties
frontendmasters.com
点明
!important是声明的旗标而非值的一部分,以及它在变量 cascade 里如何压过 specificity。 -
CSS · !important
developer.mozilla.org
!important如何改变 cascade 优先级,以及它作用于「声明」而非「值」的定位。
IACVT
- CSS: What is IACVT? bram.us Bramus 用 CSS value processing 的各个阶段定位 IACVT 发生在哪一步,以及它和「解析期报错」的本质区别。
-
Using CSS custom properties · Invalid values
developer.mozilla.org
代换后非法时
unset的落点 —— 可继承属性取父值、不可继承属性回 initial。
token 流代换
- CSS · calc() developer.mozilla.org 「无单位数 × 1unit」完成数值到长度 / 角度的类型转换 —— var() 接单位的正解。
@property
-
CSS · @property
developer.mozilla.org
@property的syntax/inherits/initial-value描述符,注册后变量的类型检查与可动画行为。 -
CSS.registerProperty()
developer.mozilla.org
JS 侧等价物:运行时用
CSS.registerProperty()注册带类型的自定义属性。
空格 hack
-
The var() space toggle hack
verou.me
Lea Verou 提出用空值与
initial两个取值,让一个自定义属性当多路开关用。
规范总参考
-
CSS Custom Properties for Cascading Variables Module Level 1
drafts.csswg.org
规范本体:把 custom property 定义为「被所有属性接受的代换值」,并给出
var()的代换语义、guaranteed-invalid value、fallback、循环引用与 IACVT 的规范条文。 -
CSS · 自定义属性 --*
developer.mozilla.org
custom property 的继承、
var()语法、@property的syntax/inherits/initial-value与CSS.registerProperty()。 -
Can I use · CSS Variables (Custom Properties)
caniuse.com
实时浏览器支持矩阵:
--*自定义属性与var()的全局可用性。 -
Can I use · CSS @property
caniuse.com
实时浏览器支持矩阵:
@propertyat-rule 与CSS.registerProperty()的全局可用性。