最轻量可靠的暗色模式方案是用 :root 声明变量,配合 @media (prefers-color-scheme: dark) 兜底和 .dark 类手动切换;所有变量必须写在 :root 中以确保全局可继承,避免 Shadow DOM、兄弟节点或原生控件读不到;媒体查询需置于默认声明之后且不可嵌套;JS 切换仅操作 class,初始化要早于首屏渲染并同步持久化;变量命名须统一前缀、拼写准确、带单位、禁用 !important,且需语义配对确保视觉一致性。

:root 声明变量 + @media (prefers-color-scheme: dark) 覆盖,再加一个 .dark 类供用户手动切换,三者不冲突、不覆盖、各司其职——这是目前最轻量也最可靠的方案。
为什么必须把所有变量写在 :root 里?
CSS 自定义属性靠继承传播,而只有 :root(即 <html> 元素)能确保任意元素通过 var(--bg-color) 拿到值。写在 .dark、body 或组件类里,变量只对该选择器及其后代生效,button、input、伪元素等兄弟节点或原生控件根本读不到。
-
.dark { --bg-color: #121212; }→input若没被包裹进.dark,就 fallback 到透明或浏览器默认色 -
body { --text-color: #eee; }→var(--text-color)在<html>上求值时根本找不到定义 - Shadow DOM 内部不会继承外部
:root变量,得在:host或::slotted里手动传入
@media (prefers-color-scheme: dark) 怎么写才不白写?
它只负责“系统偏好兜底”,不是万能开关。写错位置或漏声明变量,一刷新就崩。
- 必须放在
:root默认声明之后,否则旧版 Safari 直接忽略 - 所有变量必须先在
:root中完整声明默认值,否则系统设为no-preference时var(--border-color)会 fallback 成transparent - 不能只覆盖部分变量:漏了
--shadow-sm,用了box-shadow: var(--shadow-sm)的按钮就没了阴影 - 别嵌套写:
@media (max-width: 768px) { @media (prefers-color-scheme: dark) { ... } }是无效结构
用户点按钮切换时,JS 该做什么、不该做什么?
核心是只操作 class,别碰 style.setProperty() —— 后者会废掉整套媒体查询逻辑,且维护爆炸。
- ✅ 正确做法:
document.documentElement.classList.toggle('dark') - ❌ 绝对避免:
document.documentElement.style.setProperty('--bg-color', '#121212') - 初始化必须早于首屏渲染:在
<script>标签内直接执行,别等DOMContentLoaded - 同步持久化:
localStorage.setItem('theme', newTheme)和classList必须一起做,缺一不可 - 检查 class 名是否匹配 CSS 中的
:root.dark,JS 切dark但 CSS 写.dark-mode就完全失效
变量命名和 fallback 容易踩哪些坑?
拼错变量名、没加前缀、fallback 写法不当,都会导致样式静默崩溃,极难 debug。
立即学习“前端免费学习笔记(深入)”;
- 所有变量名统一加前缀,比如
--theme-bg-primary,避免和 Tailwind 的--tw-或未来原生属性冲突 -
color: var(--text-primary, #333)看似安全,但一旦拼错成--text-prmiary,浏览器直接回退到#333,深色下文字就看不见了 - 值要带单位:颜色写
#121212,长度写1rem,阴影写0 1px 2px rgba(0,0,0,0.1),别漏 - 别用
!important覆盖变量——它对var()无效,只能靠选择器权重或声明顺序控制
--bg-color: #000,但忘了同步改 --text-color 或 --border-color,按钮边框就消失、文字就看不清——CSS 变量只是管道,内容还得人来配对。


















