必须用 class 或 data-theme 手动控制主题切换,因 prefers-color-scheme 仅响应系统设置变更,不响应用户点击;:root 需设默认值防变量无效;初始化须在首屏渲染前完成,transition 应加在 body 等渲染元素上。

直接用 :root + @media (prefers-color-scheme: dark) 无法实现“一键切换”——它只响应系统设置,用户点按钮毫无反应。真要支持手动切换,必须靠 class 或 data-theme 控制,且初始化逻辑必须在首屏渲染前完成。
为什么不能只靠 @media (prefers-color-scheme: dark)
这个媒体查询只在两个时机生效:页面首次加载时读取系统偏好、系统设置变更时重新计算。它不监听 DOM 事件,也不响应用户点击。常见错误是写了媒体查询就以为能切换,结果按钮点了没反应,刷新后还回退到系统默认值。
更隐蔽的问题是兼容性:prefers-color-scheme 在 iOS 13 以下 Safari 中支持不稳定,部分安卓 WebView 也存在 fallback 失效现象。纯靠它,等于放弃了一部分真实用户。
:root 必须定义默认值,否则变量未定义
CSS 变量没有“继承失败就用浏览器默认”的兜底机制。如果只在 @media 里写深色值,而 :root 里没设浅色默认值,那么系统为亮色时所有 var(--bg-color) 都会解析为 invalid,最终 fallback 到透明或浏览器初始值,页面可能一片空白或文字不可见。
正确写法是:
:root {
--bg-color: #fff;
--text-color: #333;
--border-color: #ddd;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #121212;
--text-color: #e0e0e0;
--border-color: #333;
}
}
注意:@media 规则必须是顶层的,不能嵌套在 .dark 或其他选择器内部;变量名保持语义化,不要带 light/dark 后缀。
手动切换必须用 class 控制,且初始化不能等 JS 加载
切换动作本身很简单:document.documentElement.classList.toggle('dark')。但关键在初始化——90% 的“白闪”或“黑闪”,是因为 class 被加在 DOMContentLoaded 之后,浏览器已经完成首屏绘制才去改样式。
必须三步同步做:
- 在
内联极简脚本,同步读取localStorage.getItem('theme') - 立即根据值设置
document.documentElement.classList.add('dark')(或remove) - 按钮点击后,同步执行
classList.toggle和localStorage.setItem('theme', ...)
漏掉任一环,用户刷新即丢失偏好。别用 style.setProperty() 逐个设变量,那会覆盖媒体查询逻辑、卡 UI 线程、维护爆炸。
transition 加在 body 上才有效
给 :root 或 html 加 transition 没用,因为它们不是渲染盒,不触发视觉重绘。真正起作用的是实际参与布局和样式的元素,比如 body、main 或通用容器。
只对支持过渡的属性加动画:
- ✅
background-color、color、border-color - ❌
filter、opacity、font-size(除非你显式声明)
示例:
body {
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
}
别忘了检查对比度是否达标——深色模式下,#fff 文字配 #000 背景虽对比度高,但 OLED 光晕+暗光疲劳问题会让用户实际体验变差。用 #e0e0e0 / #121212 这类组合更稳妥。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











