先记住这个答案
CSS 自定义属性(-- 开头的变量)和 color、font-size 一样,天然沿 DOM 树向下继承。声明在 :root 上,页面里所有元素都能通过 var(--x) 读到。如果只想让某个组件内部用,就把声明放在组件的根选择器上,比如 .card { --card-bg: #fff },这样只有 .card 的后代元素能继承到这个变量。
- 声明在哪个选择器,就只对那棵子树生效
- 后代可以重新声明同名变量覆盖祖先
- 自定义属性能穿透 Shadow DOM 边界
var()的回退值只在变量未定义时生效
全局和组件级的声明位置
做主题切换时,通常把主题色声明在 :root 上,所有组件直接用 var(--primary) 就能拿到。但组件库里每个组件有自己的间距和圆角,这些变量没必要暴露给全局。
/* 全局主题变量 */
:root {
--primary: #1677ff;
--radius: 8px;
}
/* 组件自己的变量,只在 .card 后代中生效 */
.card {
--card-padding: 16px;
--card-bg: var(--primary); /* 可以引用全局变量 */
padding: var(--card-padding);
background: var(--card-bg);
}.card 外面的元素写 var(--card-padding) 拿不到值,因为 --card-padding 只在 .card 这个选择器上声明,继承链到不了外面。这就是最常用的组件作用域写法:不需要额外语法,利用 CSS 继承的天然特性就能做到。
后代覆盖和 Shadow DOM 穿透
同一个变量名,后代元素可以重新声明来覆盖祖先的值。这个机制和普通继承属性一样,比如子元素写 color: red 就会覆盖父元素设的蓝色。
.card {
--card-bg: white;
}
/* 深色模式下的卡片 */
.card.dark {
--card-bg: #1a1a1a;
}
/* 子组件想用不同的背景 */
.card .card-header {
--card-bg: #f5f5f5;
}.card-header 内部的后代读 var(--card-bg) 会拿到 #f5f5f5,不会被外层的 white 影响。这个覆盖遵循正常的层叠规则:对同一个元素,特异性更高或者更靠后的声明会胜出。
Web Components 使用 Shadow DOM 隔离样式。普通 CSS 选择器从外面选不到 shadow tree 里的元素,但自定义属性不受这个限制——宿主元素上声明的 --primary,shadow tree 里的后代仍然能通过 var(--primary) 拿到。这是 Shadow DOM 组件暴露样式接口的主要方式:组件内部用 var(--btn-color, blue) 读变量,外部通过在宿主或祖先上声明 --btn-color 来定制样式。
var() 回退值和无效值的坑
var(--x, fallback) 的第二个参数是回退值,但它只在变量完全未定义时才生效。如果变量声明了但值对当前属性无效,回退值不会启用。
.box {
--size: red; /* 声明了,但 red 不是合法的长度值 */
width: var(--size, 100px); /* 不会回退到 100px */
}浏览器看到 --size 有值就直接用,把 red 赋给 width。这时 width 收到一个无效值,最终使用的是 width 的初始值 auto,而不是回退值 100px。这种行为叫"保证无效值"(guaranteed-invalid value),是面试常考的细节。
要防止这个问题,可以用 @property 注册变量类型。注册后浏览器在变量赋值阶段就能做类型检查,类型不对的值会被丢弃,回退值才能正常生效。
容易答错的地方
- 以为自定义属性和普通属性一样受选择器隔离
- 普通 CSS 选择器匹配不到 Shadow DOM 里的元素,但自定义属性靠继承传递,不需要选择器匹配。外面声明的
--theme会一路继承到 shadow tree 最深处。只有 shadow tree 内部重新声明同名变量,才能覆盖外部的值。 - 以为
var()回退值能兜底一切 - 回退值只在变量未定义时生效。变量定义了但值不对,回退值不会启用,属性直接变成初始值。排查样式不生效时,先在开发者工具里看变量的计算值,确认它是未定义还是值无效。
面试官还会怎么问?
@scope 和自定义属性的作用域有什么关系?
@scope 是一个较新的 CSS 特性,用来限定选择器的匹配范围,和自定义属性的继承作用域是两回事。@scope (.card) { .title { color: red } } 让 .title 只匹配 .card 内部的元素,但它不影响自定义属性的继承。两者可以配合使用,但解决的是不同的问题。
组件库的主题变量应该声明在哪里?
通常全局主题变量放在 :root 上,组件私有变量放在组件选择器上。组件内部用 var(--comp-x, 默认值) 读取,这样外部可以选择性覆盖,不覆盖就用默认值。这个模式和 Web Components 的 Shadow DOM 方案思路一致。
自定义属性的继承和 color 的继承有什么不同?
机制完全一样,都是沿 DOM 树向下传递。区别是 color 等属性有预定义的初始值和继承性,而自定义属性的初始值默认是一个保证无效值,只有被显式声明过才有意义。另外,自定义属性默认都是可继承的,不存在"不可继承的自定义属性",除非用 @property 注册时把 inherits 设为 false。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。