@media (prefers-color-scheme: dark) 生效需满足变量默认声明在 :root、媒体查询块紧随其后、避免嵌套及动态插入、手动切换时操作 document.documentElement 而非 body,且 SSR 需内联样式并同步 data-color-scheme 属性。

@media (prefers-color-scheme: dark)是唯一可靠方式,但直接写就失效——它必须配合变量声明顺序、作用域和加载时机才能真正生效。
为什么 @media (prefers-color-scheme: dark) 写了却没反应
不是语法错,而是浏览器按 CSS 声明顺序解析,媒体查询本身不提升选择器权重。常见失效场景包括:
-
@media (prefers-color-scheme: dark)块写在:root变量定义之前:旧版 Safari(12.1–13.x)直接忽略整块,变量根本不会覆盖 - 把暗色规则写在亮色规则前面:后面无条件的
:root { --bg: #fff; }会冲掉前面媒体查询里的赋值 - 嵌套在其它媒体查询里,比如
@media (max-width: 768px) { @media (prefers-color-scheme: dark) { ... } }:Safari ≤16.4 和部分 Android WebView 不识别 - 用 JS 动态插入
<style></style>标签再写该媒体查询:部分浏览器不触发匹配逻辑
:root 变量必须先声明默认值,再在媒体查询中覆盖
漏掉任意一个变量的默认声明,对应 var(--x) 在非暗色场景下就会 fallback 到 transparent 或初始值,导致文字消失、边框透明、按钮不可见。
- 所有主题相关变量(如
--bg、--text、--border、--shadow)必须在顶层:root中完整声明一次,哪怕值和亮色一致 -
@media (prefers-color-scheme: dark)块内只能用相同变量名重新赋值,不能新增变量 - 不要只改背景色而漏掉
--border-color:用了border: 1px solid var(--border-color)的元素会变成 1px 透明边框 - 避免在媒体查询里重复声明无关属性(如
font-family),纯属冗余
手动切换时别用 body 或组件加 class
用户点击「深色模式」按钮后,JS 改 document.body.classList.add('dark') 是无效的——:root 是 元素,变量继承链从这里开始,改 body 不会触发变量重计算。
- 正确做法是操作
document.documentElement.classList.add('dark') - 配套 CSS 必须写成
html.dark :root { --bg: #121212; },而不是.dark { --bg: #121212; } - 如果同时用了
@media (prefers-color-scheme: dark)和手动 class 切换,两者不要混用同一组选择器(比如都写:root),否则 specificity 冲突难调试 - 禁用
filter: invert()或transform: brightness()这类伪暗色方案:它们破坏可访问性、干扰表单控件、绕过语义化颜色系统
别指望它实时响应系统主题切换
@media (prefers-color-scheme: dark) 只在页面加载时读取一次系统偏好。用户中途在 macOS/Windows/iOS 上切回浅色模式,已打开的页面不会自动重绘——除非刷新,或你主动监听 matchMedia 并强制更新 DOM 类名或变量。
- Safari ≤16.4 和某些 Android WebView 中,甚至不支持
matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ...) - 如果你需要运行时响应,只应在
DOMContentLoaded后读一次matchMedia状态,不建议监听 change 事件,除非你已处理好样式抖动和伪元素重绘问题 - SSR 场景下必须内联关键样式,并同步设置
,否则首屏会闪白
最易被忽略的一点:所有变量必须在 :root 中声明默认值,且媒体查询块必须紧随其后——顺序错了,整个暗色逻辑就断在第一步。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











