最轻量方案是:root配合@media (prefers-color-scheme: dark)零JS响应系统主题,变量必须定义在:root才可被媒体查询覆盖,且媒体查询需与:root平级、后置;手动切换需用data-theme属性+CSS优先级控制,并注意图片、阴影、过渡等细节。

直接用 :root + @media (prefers-color-scheme: dark) 就能零 JS 自动响应系统设置,这是目前最轻量、兼容性最好、维护成本最低的方案。
为什么必须把变量定义在 :root 里
只有 :root 下的 CSS 变量才是全局可继承、可被媒体查询覆盖的。如果写在 .card 或 body 里,@media (prefers-color-scheme: dark) 根本无法修改它——浏览器不会跨选择器层去“重写”局部变量。
常见错误是这样写:
:root { --bg: #fff; }
.card { background: var(--bg); }
@media (prefers-color-scheme: dark) {
.card { background: #121212; } /* ❌ 覆盖的是具体样式,不是变量 */
}正确做法是只在 :root 中定义和覆盖变量:
立即学习“前端免费学习笔记(深入)”;
-
:root中设默认值(浅色) -
@media (prefers-color-scheme: dark)块内只改:root的变量值,不写任何组件选择器 - 所有组件统一用
var(--bg),不硬编码颜色
@media (prefers-color-scheme: dark) 必须写在顶层
这个媒体查询不能嵌套在任何选择器内部,比如不能写成 body {@media (...) {...}},否则会被忽略。它必须和 :root 平级,作为独立规则块存在。
还要注意顺序:默认变量必须先声明,媒体查询后出现。反过来会导致未匹配时变量未定义,var(--bg) fallback 到 initial 或透明背景。
另外别漏掉这行:
html { color-scheme: light dark; }否则滚动条、<input>、<select> 等原生控件不会随主题变色。
手动切换时怎么避免和系统媒体查询冲突
如果既要支持系统自动适配,又提供用户手动开关,就不能只靠 prefers-color-scheme。得用 data-theme 或 class 控制,并让 CSS 层级优先于媒体查询。
关键点:
- 给
html元素加data-theme="dark"或data-theme="light" - CSS 中用属性选择器覆盖,例如:
html[data-theme="dark"] :root { --bg: #121212; } - 这个规则必须写在
@media块之后,才能压过它 - JS 切换时同步写入
localStorage,刷新后读取并设置data-theme - 首次加载时,先查
localStorage,再 fallback 到matchMedia查询系统偏好
图片、阴影、border 在暗色模式下容易翻车
CSS 变量能管颜色,但管不了图片明暗或滤镜效果。纯白 PNG 在深色背景上会刺眼,纯黑 SVG 图标可能消失。
解决方式很实际:
- 用
picture+source[media]按主题切图源:<source media="(prefers-color-scheme: dark)" srcset="icon-dark.svg"> - 对图标类元素加
filter: brightness(0.85)微调,避免全黑 -
box-shadow别用固定rgba(0,0,0,0.2),改用变量:box-shadow: 0 2px 8px var(--shadow-color);,并在:root里分别定义明/暗两套 - 慎用
background-blend-mode或mix-blend-mode,它们在不同主题下行为不可控
最易被忽略的是过渡动画:所有用到变量的颜色都要加 transition,比如 background-color 0.3s, color 0.3s,否则切换时会闪一下——而这个闪,在 SSR 渲染场景下尤其明显,因为首屏 HTML 是按服务端判断的主题生成的,客户端 JS 还没执行完就先渲染了一帧旧主题。


















