必须把所有主题变量定义在:root里,因为CSS自定义属性依赖继承传播,而只有:root(即<html>元素)能确保全页面任意元素通过var()正确读取;写在.body、.dark等选择器内会导致未被包裹的表单控件等回退失效,且默认:root块必须完整声明所有变量,否则渲染即fallback。

必须把所有主题变量定义在 :root 里,否则切换必然失效——这不是可选项,是 CSS 自定义属性作用域的硬性限制。
为什么只能用 :root 声明变量
CSS 自定义属性靠继承传播,而只有 :root(即 <html> 元素)能确保任意元素通过 var(--bg-color) 正确读取。写在 .dark、body 或组件类里,变量只在该选择器匹配的子树生效:
-
.theme-dark { --bg-color: #121212; }→button或input若未被包裹,就 fallback 到透明或浏览器默认值 -
body { --text-color: #eee; }→var(--text-color)在<html>上求值时根本找不到定义 - Shadow DOM 内部不会继承外部
:root变量,需在:host或::slotted里手动传入
:root 里怎么写变量才不踩坑
变量名要语义化、值要带单位、所有主题下用到的变量必须在默认 :root 块中完整声明——哪怕深色模式下值相同,也不能漏:
- 正确:
:root { --bg-primary: #ffffff; --text-primary: #1a1a1a; --border-color: #e0e0e0; --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); } - 错误:只在
[data-theme="dark"] :root里声明--shadow-sm,但默认:root没定义 → 页面一渲染就 fallback 成transparent - 避免变量名含空格或非法字符:
--font size lg无效,--font-size-lg才合法 - 用
rem或em写变量值时,必须确保根字体大小已设好,否则响应式错乱
手动切换时别碰 style.setProperty()
调用 document.documentElement.style.setProperty('--bg-primary', '#121212') 看似简单,实际埋了三个雷:
立即学习“前端免费学习笔记(深入)”;
- 会破坏
@media (prefers-color-scheme: dark)的自动响应逻辑 - 大量变量逐个设置会卡 UI 线程,尤其在低端设备上明显
- 维护成本爆炸:新增一个颜色就得同步改 JS 和 CSS 两处
真正轻量的做法是只操作 class:document.documentElement.classList.add('dark'),所有变量值由 CSS 规则自动重算。
初始化必须早于首屏渲染,且联动 localStorage
页面闪屏(白底→黑底)90% 是因为 class 没在 DOM 加载完成前设置。初始化逻辑必须三步到位:
- 读取
localStorage.getItem('theme'),而非依赖matchMedia初始值 - 在
<script>标签内直接操作document.documentElement.classList,不要等DOMContentLoaded - 切换按钮点击后,同步执行
classList.toggle('dark')和localStorage.setItem('theme', newTheme)
漏掉任一环,用户刷新后主题就回退,或者原生控件(<input>、<select>)仍按旧主题渲染。


















