CSS局部变量作用域由声明它的选择器决定,只在匹配元素及其后代中生效,不随class切换而改变,遵循DOM树就近原则而非选择器权重,调试应依赖DevTools的computed面板。

局部作用域变量必须定义在具体选择器内,不能靠 class 切换“覆盖”
CSS 变量的局部作用域不是靠添加/移除 class 来“激活”,而是由声明它的选择器决定的——变量只在该选择器匹配的元素及其所有后代中生效。比如 .card { --card-bg: #fff; },这个 --card-bg 就只对 .card 元素本身和它内部的 .card-header、.card-body 等子元素有效,隔壁另一个 .card 实例也互不干扰。
常见错误是以为写个 .theme-dark .card { --card-bg: #333; } 就能“切换变量”,其实它只在同时匹配 .theme-dark 和 .card 的那个元素树里生效;如果父容器没加 .theme-dark,这行声明根本不会被应用。
- 局部变量定义位置只能是具体的选择器规则块,比如
.modal、#sidebar、[data-section="profile"] - 不能在媒体查询或伪类里单独定义变量(如
@media (max-width: 768px) { --gap: 8px; }是无效的,必须包裹在某个选择器内) - 继承是自动发生的:只要后代元素没被更近的作用域覆盖,就能读到父级定义的
var(--xxx)
多个同名局部变量共存时,就近原则决定取值
当 DOM 树不同层级都定义了同一个变量名,比如外层 .list { --item-height: 40px; },内层 .list-item { --item-height: 48px; },那么 .list-item 自己和它的子元素读到的就是 48px,而 .list 的其他子元素(非 .list-item)仍用 40px。这种就近覆盖是 CSS 层叠机制天然支持的,不需要额外逻辑。
注意:这种就近不是“选择器权重”决定的,而是 DOM 树深度决定的。哪怕 .list-item 的选择器权重更低,只要它在更深层级定义了变量,就会覆盖外层。
立即学习“前端免费学习笔记(深入)”;
- 变量查找路径是沿 DOM 向上,直到找到第一个定义该变量的祖先(含自身),而不是按 CSS 规则顺序或 specificity 匹配
- 如果某元素既不在
.card内也不在.modal内,但用了var(--card-bg),且全局没定义,那结果就是unset(最终计算值为 inherit 或初始值,取决于属性) - 可以用
var(--color, #000)提供 fallback,但 fallback 不参与作用域查找
:root 定义的是全局变量,别误当成“可开关的默认值”
:root { --primary: #007bff; } 确实让 --primary 在全页面可用,但它仍是“全局变量”,不是“默认值开关”。一旦你在某个局部选择器里重新定义 --primary,比如 .dashboard { --primary: #28a745; },那整个 .dashboard 子树就用绿色,跟 :root 里的蓝色完全无关——这不是覆盖,是新建一个同名局部变量。
所以不要指望靠改 :root 里的值来“统一更新所有主题”,除非你确保所有地方都只读它、不重定义。实际项目中更常见的是把 :root 当作兜底 fallback,真正控制逻辑放在组件级局部变量里。
- 全局变量适合放真正跨模块一致的常量,比如
--font-family-base、--breakpoint-md - 主题色、间距、圆角这些易变项,更适合用局部变量 + class 控制(如
.theme-dark { --bg: #1a1a1a; }) - DevTools 里看 computed 值时,鼠标悬停
var(--x)会显示当前生效的定义位置,这是验证作用域最直接的方式
Vue / React 组件中用局部变量要注意宿主元素边界
在 Vue 单文件组件或 React JSX 里,如果你把变量定义在组件根元素上,比如 Vue 的 <template><div class="user-card">...</div></template> 对应的 .user-card { --user-border: 1px solid #eee; },那这个变量只对该组件渲染出的 DOM 树有效。但要注意:如果子组件渲染的节点脱离了这个根元素(比如用 Teleport 渲染弹窗),那些节点就访问不到 --user-border,因为它们已不在原 DOM 子树中。
另外,CSS Modules 或 scoped style 并不会自动限制变量作用域——.Button-module__root { --btn-color: blue; } 这种写法依然依赖 DOM 结构继承,不是“模块级封装”。
- 使用
Teleport或createPortal时,需手动把变量带上,比如给 teleport 目标容器也加对应 class - 不要依赖
scoped的选择器哈希来“隔离变量”,CSS 变量不认这个,只认真实 DOM 层级 - SSR 场景下,服务端生成的 HTML 若没包含变量定义的 class,客户端 JS 补上后变量才生效,可能造成 FOUC


















