暗黑模式必须通过 html[data-theme="dark"] 控制,而非 BEM 类名中的 --dark 修饰符;否则会破坏语义、导致 SSR 白屏、被 PurgeCSS 误删且无法响应系统偏好。

暗黑模式不该靠类名切换,而该靠 html[data-theme="dark"] 控制变量;所有 BEM 类名里硬塞 --dark 或 --theme-dark 都是错的——它破坏语义、导致 SSR 白屏、被 PurgeCSS 误删,且无法响应系统偏好。
为什么不能用 button--dark 这类修饰符
这类写法把环境状态(当前主题)和组件角色(按钮)耦合了:button--dark 暗示“这个按钮天生就是暗色”,但实际它只是在暗色主题下应呈现某种颜色。问题会立刻暴露:
- 换主题时得全局搜
button--dark、card--dark、input--dark……改不完 - SSR 渲染时 JS 没加载,
button--dark类存在但对应 CSS 被 PurgeCSS 删了,按钮直接无样式 - 用户开系统暗色偏好,但页面没加
button--dark类,界面一半亮一半暗 -
button--dark和button--primary组合时,权重难控,容易覆盖出 bug
html[data-theme="dark"] 是唯一合法的根级控制方式
主题必须挂载在 html 元素上,用 data-theme 属性,不是 body 类、不是 :root.dark、也不是自定义容器类。原因很实在:
- 浏览器能原生响应
window.matchMedia('(prefers-color-scheme: dark)')并自动增删data-theme值,无需手动监听 - CSS 变量可天然继承并 fallback:
color: var(--text-primary, #333),JS 失败或 SSR 阶段也能保底 - 服务端可直接根据 Cookie 或 UA 输出
<html data-theme="dark">,首屏不白屏 - PurgeCSS 不会误删 —— 它只删 HTML 中未出现的类,而
data-theme是属性,不在清理范围内
切换只需一行 JS:document.documentElement.dataset.theme = 'dark',别碰 body.className,也别往 app 容器上加类。
立即学习“前端免费学习笔记(深入)”;
BEM 类名里怎么响应主题?只靠变量,不靠新修饰符
组件类名保持干净,主题响应全交给 CSS 自定义属性驱动:
-
.button--primary只写变量引用:background-color: var(--bg-primary); color: var(--text-on-primary); - 主题覆盖用更具体选择器:
html[data-theme="dark"] .button--primary { --bg-primary: #1e1e1e; --text-on-primary: #fff; } - 避免写
.button--primary--dark—— 这既不是 BEM 合法修饰符(元素修饰符应为.button__label--hidden),也不符合“修饰符只表达组件自身状态”的原则 - 嵌套深的结构(如
.card__content .button--primary)也要保持同等选择器深度,或用:where()降权防权重爆炸
顶层 Block 的主题修饰符必须带 --theme- 前缀
如果你真需要一个包裹整页的 Block(比如 .app),它的主题相关修饰符只能是 .app--theme-dark,不能是 .app--dark 或 .app-dark:
-
--theme-前缀是 BEM 显式约定,用于区分状态类(--disabled)、尺寸类(--large)和主题上下文 - 多个主题叠加时写成
.app.app--theme-dark.app--theme-high-contrast,而不是.app--theme-dark-high-contrast—— 修饰符不可嵌套、不可连写 - 即便用了
.app--theme-dark,它本身也不能直接定义颜色,只用来配合html[data-theme="dark"]做局部作用域隔离(例如在 Shadow DOM 或微前端子应用中)
真正容易被忽略的是:组件内部永远不要出现 --dark 这种字眼;所有外观变化,必须收敛到变量声明与覆盖这一层。一旦在 HTML 里看到 class="button button--primary button--dark",就说明主题系统已经失控了。


















