深色模式失效的根本原因是变量未在 :root 中完整声明,导致 var() 回退为 unset;必须在 :root 中一次性定义全部主题变量,默认浅色值,并用独立后置的 @media (prefers-color-scheme: dark) 覆盖,手动切换仅限 class 操作,且需同步 localstorage 与 color-scheme 声明。

:root 必须声明所有变量,否则深色模式会失效
浏览器不会自动 fallback 到合理值——var(--bg-color) 在未定义时直接回退为 unset,导致 body 透明、文字消失。这不是媒体查询没生效,而是变量根本不存在。
必须在 :root 块中一次性声明全部主题变量,默认用浅色值:
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--border-color: #e0e0e0;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
}
- 漏一个变量(比如没写
--shadow-sm),用了box-shadow: var(--shadow-sm)的组件就失去阴影 - 变量名拼错(如
--text-clor)会导致 fallback 生效,但你可能误以为是颜色不对,实际是变量未定义 - 不要在
.dark或body里声明变量——input、svg、伪元素等兄弟节点读不到
@media (prefers-color-scheme: dark) 必须后置且独立
它只负责系统偏好兜底,不是万能开关。写错位置或嵌套,旧版 Safari 直接忽略整块规则。
正确结构只能是:
:root { /* 浅色默认值 */ }
@media (prefers-color-scheme: dark) {
:root { /* 深色覆盖值 */ }
}
- 不能嵌套:
@media (max-width: 768px) { @media (prefers-color-scheme: dark) { ... } }是无效语法 - 不能前置:深色规则必须放在所有
:root默认声明之后,CSS 按顺序覆盖,不看“媒体查询优先级” - 不能只覆盖部分变量——漏了
--border-color,边框就会用浏览器默认色(通常是浅灰),破坏一致性
手动切换必须用 class,禁用 style.setProperty()
用户点按钮切深色,JS 只能操作 document.documentElement.classList.toggle('dark')。一旦调用 style.setProperty(),整套媒体查询逻辑就废了。
- ✅ 正确:
document.documentElement.classList.add('dark') - ❌ 危险:
document.documentElement.style.setProperty('--bg-color', '#121212')—— 这会覆盖:root.dark规则,且后续系统偏好变化完全失效 - 初始化必须早于首屏渲染:把 JS 放在
里立即执行,别等DOMContentLoaded,否则闪屏 - 同步持久化:
localStorage.setItem('theme', 'dark')和classList必须同时做,缺一不可
原生控件和 Shadow DOM 不继承变量,需显式处理
input、textarea、滚动条、svg 图标、::placeholder 等完全不响应 var(--text-color),它们不读自定义属性,也不继承主题。
- 必须给
input显式设样式:background: var(--bg-surface); color: var(--text-primary); -
svg的fill和stroke不能靠currentColor自动适配,得写成fill: var(--icon-primary); -
::-webkit-scrollbar-thumb等伪元素要单独写规则,且必须用变量,不能指望它跟随主题 - Shadow DOM 内部不会继承外部
:root变量,得在:host或::slotted里手动传入
最易被忽略的是 color-scheme: light dark 没加在 html 或 :root 上——iOS Safari 会强制渲染浅色表单,无论变量怎么设。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











