必须将所有主题变量声明在 :root 中,确保全页面任意节点都能访问;媒体查询需完整覆盖整套变量;手动切换应通过 class 控制而非 setProperty;所有 var() 必须带可用 fallback 并加语义前缀。

必须把所有主题变量声明在 :root 里
写在 .dark、body 或组件类里,变量只对后代生效,input、button、伪元素等兄弟节点根本读不到——继承链断了。浏览器查 var(--bg-color) 是从当前元素向上找最近的定义,而只有 :root(即 html 元素)能保证全页面任意节点都能触达。
常见错误:
-
.theme-dark { --bg-color: #121212; }→ 只影响该元素及其子树,表单控件不响应 -
body { --text-color: #eee; }→html上求值失败,直接 fallback 到 transparent 或浏览器默认色 - 只在
[data-theme="dark"] :root中声明变量,但默认:root没定义--shadow-sm→ 首屏渲染时阴影消失
@media (prefers-color-scheme: dark) 必须完整覆盖 :root 变量
媒体查询不是“补充深色值”,而是“重置整套变量”。如果浅色模式下定义了 --bg-primary 和 --text-primary,深色分支里漏掉 --border-color,那用到它的组件就会 fallback 到初始值或透明,样式崩得悄无声息。
正确写法是两套独立、完整、语义一致的声明:
立即学习“前端免费学习笔记(深入)”;
:root {
--bg-primary: #ffffff;
--text-primary: #1a1a1a;
--border-color: #e0e0e0;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #121212;
--text-primary: #e0e0e0;
--border-color: #333333;
}
}
注意:@media 块必须写在 :root 外层,不能嵌套进选择器;变量名保持稳定(如统一用 --bg-primary,而非 --light-bg/--dark-bg),否则切换逻辑就变成显隐切换,不是状态映射。
手动切换时用 document.documentElement.classList.toggle('dark'),别碰 style.setProperty()
直接调 document.documentElement.style.setProperty('--bg-primary', '#121212') 看似快,实则破坏整个变量体系:它会覆盖 @media 规则,让系统偏好失效;后续再切回浅色,JS 得手动还原所有变量,维护爆炸。
真正可维护的做法是:
- 给
html元素加class="dark"(或data-theme="dark") - 用
html.dark :root或html[data-theme="dark"] :root再次声明变量,优先级高于@media(比如写在它后面) - 初始化时立刻读取
localStorage.getItem('theme')并同步设置 class,否则首屏必闪 - 点击切换后,必须同步
localStorage.setItem('theme', newTheme),否则刷新就丢状态
所有 var() 调用必须带 fallback,变量名强制加前缀
color: var(--text-primary, #333) 不是保险绳,而是 debug 难点:拼错变量名(如 --text-prmiary)时浏览器静默 fallback,样式错位却无报错。更糟的是,第三方库(如 Tailwind)可能也用 --tw- 前缀,撞名直接覆盖。
防踩坑要点:
- 所有变量名统一加语义前缀,例如
--theme-bg-primary、--theme-text-secondary - 每个
var()都写 fallback,且 fallback 值要真实可用(background: var(--theme-bg-card, #f9f9f9)) - Shadow DOM 内部不会继承外部
:root变量,需在:host或::slotted里显式传入 - 加
color-scheme: light dark告诉浏览器你支持双模式,原生控件(input、select)才自动适配


















