必须用 :root 声明变量,因其优先级最高(0-1-0)、语义明确、兼容 Shadow DOM 和 SVG;切换须用 [data-theme="dark"] :root 覆盖,确保权重可靠且语法合法;JS 切换时需预设初始值、同步 localStorage、声明所有变量并配合法 fallback。

必须用 :root 声明变量,且切换必须靠 data-theme 属性 + CSS 层叠覆盖,而不是改 class 或直接操作 style.setProperty 乱设未声明的变量。
为什么只能写 :root,不能写 html 或 body
因为 :root 的选择器优先级是 0-1-0,html 是 0-0-1,body 更低;在 Shadow DOM、旧版 Safari 或多层嵌套组件中,body { --color: red } 声明的变量可能根本无法被子元素继承。更关键的是::root 是 CSS 规范定义的“文档作用域起点”,语义明确,且在 SVG/XML 中仍有效——html 则只适用于 HTML 文档。
常见错误包括:html:root(语法非法)、body { --gap: 1rem }(子元素读不到)、漏加分号导致后续变量静默失效(如 --size: 16px 后没分号,--color 就不注册)。
:root[data-theme="dark"] 覆盖变量比 .dark :root 可靠
用属性选择器 [data-theme="dark"] :root 而不是类选择器 .dark :root,是因为前者权重固定为 110,远高于普通 class(10),不会被业务中已有的 .dark-mode-toggle 等类意外干扰。更重要的是:.dark :root 要求 :root 是 .dark 的后代——但 :root 永远是顶层元素,不可能有父节点,所以这种写法实际无效。
正确结构示例:
:root {
--bg: #fff;
--text: #333;
--border: #ddd;
}
[data-theme="dark"] :root {
--bg: #1a1a1a;
--text: #e0e0e0;
--border: #444;
}
注意:所有主题变量必须成套覆盖,漏掉 --border,暗色模式下边框还是浅灰,视觉断裂。
JS 切换时最容易漏掉的三件事
不是调用 dataset.theme = 'dark' 就算完事:
- 没在
内联脚本中预设初始值,首屏必闪屏:<script>document.documentElement.dataset.theme = localStorage.getItem('ui-theme') || 'light';</script> - 只改
dataset.theme却忘了同步存localStorage.setItem('ui-theme', 'dark'),刷新后回退到默认 - 用
style.setProperty('--hue', '240')动态设值,但:root里根本没声明--hue,浏览器找不到定义,var(--hue)回退为transparent或继承值,整页变空
var(--x, #333) 的 fallback 不是可选项,是容错刚需
var() 不是取值函数,而是运行时向上查找表达式。一旦拼写错误(--text-color vs --text-colour)、变量未声明、或值非法(如 --size: 12px 16px 多值未引号包裹),整条声明会被丢弃,属性直接失效——文字可能变透明,背景可能变白,而不是“继承父级颜色”。
所以必须写 fallback,且 fallback 必须是合法 CSS 值:
-
color: var(--text-color, #333);✅ -
font-size: var(--size, red);❌(red不是合法font-size值) -
background: var(--bg, var(--fallback, #fff));⚠️(嵌套var()在部分旧 Safari 中解析失败,别超两层)
真正容易被忽略的是:变量生效依赖样式规则本身是否参与计算。如果某个按钮写了 color: #333 !important;,那无论你怎么切 :root 变量,它都不会变——var() 再动态,也赢不了 !important。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











