正确做法是将主题上下文锚定在 html 元素上,用 html[data-theme="dark"] 选择器重定义 CSS 变量,禁止使用 body--dark 类、组合修饰符或分离的主题 CSS 文件,所有变量必须带 fallback。

用 html[data-theme] 替代 body--dark 类控制主题上下文
直接改 body 的 class(如 body--dark)是错的——它不是你定义的 Block,违反 BEM 命名契约;prefers-color-scheme 无法自动同步;PurgeCSS 极易误删未在首屏 HTML 中显式出现的样式;原生表单控件(select、checkbox)会因强制覆盖而失去可访问性。
正确做法是把主题上下文锚定在 html 元素上:
- JS 切换只执行
document.documentElement.dataset.theme = "dark",不操作任何 class - CSS 中用
html[data-theme="dark"] .button--primary这类选择器重定义变量,而非.button--dark - 服务端渲染时需提前根据 User-Agent 或 Cookie 写入
data-theme值,避免白屏
.button--primary 只声明角色,颜色全部交给 var(--btn-bg)
BEM 修饰符负责语义,不是外观开关。.button--primary 的含义永远是“这是一个主要操作按钮”,它不该自己写死背景色或文字色。
基础样式只引用变量:
立即学习“前端免费学习笔记(深入)”;
.button--primary {
background-color: var(--btn-bg);
color: var(--btn-text);
border-color: var(--btn-border);
}
主题覆盖必须更具体、作用域明确:
- 禁止写
:root.dark { --btn-bg: #1e1e1e; }——:root不支持属性选择器 - 必须写
html[data-theme="dark"] .button--primary { --btn-bg: #1e1e1e; } - 所有变量必须带 fallback:
background-color: var(--btn-bg, #007bff);
避免 .button--primary.button--dark 这类组合修饰符
这种写法看似直观,实则破坏正交性:它把“功能角色”和“主题状态”耦合进一个选择器,导致复用断裂、权重失控、调试困难。
常见问题包括:
- 切换主题后,
.button--disabled的图标颜色没变(因为--btn-text没在html[data-theme="dark"]下重定义) -
.card__header .button--primary在暗色模式下仍用亮色背景(主题选择器没同步加深层级) - 多个主题 CSS 文件分开打包(
theme-light.css/theme-dark.css),加载顺序不确定,变量被意外覆盖
正确解法是让主题变量作用域覆盖所有相关组件层级,并确保每个主题分支下,所有依赖变量(--card-bg、--input-border、--text-color)一次性重定义。
所有主题样式必须收在按组件组织的单文件中,而非按主题拆分
拆成 light.css + dark.css 是高风险操作:构建工具无法保证加载顺序;热更新时容易漏掉某一份;开发者难以定位某个变量到底在哪份里被覆盖。
推荐结构是按组件维度组织:
-
button.css:含.button基础样式、.button--primary角色定义、html[data-theme="dark"] .button--primary变量重定义 -
card.css:同理,所有主题变量都在同一文件内闭环 - PostCSS 或 stylelint 需配置
ignoreSelectors放行html[data-theme="dark"] .button--primary类型选择器,否则会被 BEM 规则误报
最常被忽略的一点:变量 fallback 不只是防 JS 加载失败,更是 SSR 首屏渲染的生命线。没 fallback 的 var(--theme-color) 在服务端直出时就是 undefined,浏览器会回退到继承值或透明,视觉直接断裂。


















