必须把变量定义在:root上,因其是唯一能确保所有节点(含原生控件、伪元素、Shadow DOM)继承的全局作用域;写在.brand-a或body中会导致作用域受限、fallback失效或第三方组件失联。

必须把变量定义在 :root 上,用 document.documentElement.classList 切换主题类,否则第三方组件、伪元素、<input> 都会失效或闪屏。
为什么不能写在 .brand-a 或 body 里
CSS 变量靠继承传播,而只有 :root(即 <html>)能确保所有节点——包括 <button>、<input>、::before、Shadow DOM 内部——都能读到。写在其他选择器里,作用域就受限了:
-
.brand-a { --color-primary: #0066cc }→ 只对.brand-a后代生效,<input type="date">这类原生控件根本拿不到值 -
body { --text-color: #333 }→ 在<html>元素上求值时压根没定义,var(--text-color)直接 fallback 到透明或浏览器默认 - 第三方 UI 库(如 NutUI 的
.nut-button)若未显式透传:root变量,样式就会断掉
:root.brand-a 和 :root[data-theme="brand-a"] 怎么选
两者都可行,但语义和维护成本不同:
- 用类名(
:root.brand-a)更轻量,切换只需classList.toggle('brand-a'),适合品牌数少、不常增删的场景 - 用属性(
:root[data-theme="brand-a"])更结构化,支持任意字符串主题名(比如带版本号data-theme="brand-a-v2"),也更容易跟后端租户配置对齐 - 如果要用
prefers-color-scheme做兜底,建议统一用data-theme,避免类名冲突(比如.dark和.brand-b同时存在时优先级难控)
JS 切换时最容易踩的三个坑
不是“写了变量就能切”,关键动作漏一个,用户刷新就回退、原生控件就变色、第三方组件就失联:
立即学习“前端免费学习笔记(深入)”;
- 初始化必须早于首屏渲染:在
<head>里内联 JS,直接读localStorage.getItem('theme'),立刻执行document.documentElement.setAttribute('data-theme', value),别等DOMContentLoaded - 切换按钮点击后,必须同步改
data-theme和localStorage,否则下次打开还是旧主题 - 第三方组件主题失效?检查选择器是否够具体:把
.nut-button改成html[data-theme="brand-a"] .nut-button,禁用!important—— 它会切断变量链,导致后续手动切换完全失效
SCSS/Less 只能当“变量工厂”,别指望它运行时切换
SCSS 的 $primary-color 在构建阶段就被替换成 #409eff,最终 CSS 里根本没有变量。它唯一该干的事,是批量生成多套 :root[data-theme="xxx"] 规则:
$themes: (
brand-a: (primary: #0066cc, bg: #ffffff),
brand-b: (primary: #27ae60, bg: #f8f9fa),
brand-c: (primary: #e74c3c, bg: #fdf2f2)
);
@each $name, $map in $themes {
:root[data-theme="#{$name}"] {
--color-primary: map-get($map, primary);
--color-bg: map-get($map, bg);
}
}
真正响应切换的是 JS 控制的 data-theme 属性,不是 SCSS 变量本身。混淆这两者,就是“改了 SCSS 文件却看不到页面变化”的根源。


















