先记住这个答案
var(--x) 替换后的值如果对当前属性不合法,比如 color: var(--x) 里 --x 是 100px,浏览器不会回退到样式表里写的上一条 color 声明。因为层叠排序在变量替换之前就完成了,这条含 var() 的声明已经赢了,替换后才发现值不对。这时候属性会取继承值(如果是可继承属性)或初始值(如果不可继承),而不是去找层叠中的其它规则。CSS 规范管这个叫 invalid at computed-value time,「计算值时无效」。
- 替换发生在层叠之后,回退不了上一条声明
- 变量失效时属性回到继承或初值
- 回退参数不处理已定义但无效的值
- 和语法错误不同,语法错误的声明会被直接丢弃
和语法错误完全不同
理解这个问题的关键是区分「语法错误」和「计算值时无效」。看一个对比。
.box {
color: green;
color: 100px; /* 语法错误,整行被丢弃,color 保留 green */
}
.card {
--x: 100px;
color: green;
color: var(--x); /* 层叠赢了,替换后 100px 对 color 无效 */
}.box 里 color: 100px 在解析阶段就被浏览器识别为语法错误,这条声明直接丢弃,color 用前面的 green。这是大家熟悉的 CSS 容错行为。
.card 的情况不一样。color: var(--x) 在解析阶段是合法的,因为 var() 可以展开成任何值,浏览器没法提前判断。所以它正常参与层叠,把 color: green 覆盖掉了。到了计算样式时,var(--x) 展开成 100px,浏览器发现 100px 不是合法的颜色值,但层叠已经结束了,没法回头用 green。最终 color 取继承值,也就是父元素的 color。如果没有父元素,取 color 的初始值 canvastext(通常是黑色)。
为什么不能回退到上一条声明
CSS 的处理流程是先层叠再替换。层叠阶段看的是选择器权重、声明顺序和来源,color: var(--x) 里的 var() 还没展开,浏览器把它当作一个合法的声明参与排序。排序完毕后只留下赢家,输家已经被淘汰了。等到替换阶段发现值不合法,输家们早就不在候选列表里了。
这就像考试选拔:先按笔试成绩排名选出入围者,入围后才发现体检不合格。这时候不会自动递补下一名,而是按「保底规则」处理。CSS 的保底规则就是:可继承属性取继承值,不可继承属性取初始值。
color 是可继承属性,所以取父元素的 color。如果换成 padding(不可继承),取初始值 0。
var() 回退参数和「计算值时无效」是两回事
var() 的第二个参数是回退值,写法是 var(--x, blue)。但这个回退只在变量未定义的时候触发,不在值不合法的时候触发。
.text {
--x: 100px;
color: var(--x, blue);
}--x 的值是 100px,变量是有定义的,所以 var(--x, blue) 的回退值 blue 不会被用到。var() 展开成 100px,color: 100px 在计算阶段无效,最终 color 取继承值。
要触发回退值,得让 --x 完全没有定义,也就是既没有在任何祖先上声明,也没有被设成 initial 以外的值。这个区分很容易搞混:回退值管的是「变量存不存在」,不管「变量的值对不对」。
怎样避免踩坑
实际项目里遇到这个问题通常是因为自定义属性被外层意外覆盖了,比如第三方库在 :root 上设了一个同名变量,值的类型和你期望的不一样。
防范办法有几个。第一,用有前缀的变量名(比如 --myapp-color)避免命名冲突。第二,用 @property 注册属性并指定 syntax,这样浏览器会在赋值阶段就做类型检查,不合规的值不会被接受,属性保留原来的有效值或用 initial-value。第三,在 var() 里写回退值兜底,至少在变量未定义的场景下能有合理的默认样式。
用了 @property 注册之后,行为会变:赋值时如果类型不匹配,声明直接被忽略,和语法错误一样,不会走「计算值时无效」的路径。这其实是更安全的做法。
容易答错的地方
- 以为会回退到上一条同名声明
- 这是最常见的误解。写了
color: green; color: var(--x)以为--x无效时会回到green,实际上不会。层叠在替换之前就定了,color: green已经被覆盖了。 - 以为
var()的回退值能兜住一切 var(--x, blue)只在--x未定义时生效。如果--x有值但值不合法,回退值不起作用,照样走「计算值时无效」。
面试官还会怎么问?
color 和 padding 遇到这种情况结果一样吗?
不一样。color 可继承,取父元素的 color 值。padding 不可继承,取初始值 0。判断标准就是这个属性是否可继承,和 unset 关键字的逻辑一样。
用 @property 注册之后还会出现这个问题吗?
不太会了。注册后浏览器在赋值阶段就做类型检查,类型不匹配的值直接被当作无效声明丢弃,行为和普通的语法错误一样。属性会保留之前的有效值或者用 initial-value,不会走「计算值时无效」的路径。
怎么在 DevTools 里排查这种问题?
Chrome DevTools 的 Computed 面板里,属性值旁边会显示来源。如果发现某个属性的值不是你写的那条规则给的,而是变成了继承值或初始值,可能就踩到了这个坑。在 Elements 面板里看 Styles,被计算值时无效替换掉的声明不会被划掉(因为层叠上它是赢家),但计算结果不是它的值。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。