BEM 修饰符不能直接表达主题,应使用 html[data-theme="dark"] 控制主题切换,变量需按“作用域+类型+含义”命名并带 fallback,组件内只声明角色不写具体值,避免语义断裂与样式失控。

为什么不能把主题写成 .button--dark
这是最常踩的坑:用 BEM 修饰符直接表达主题,比如 .button--dark 或 .card--high-contrast。它看起来省事,但实际会让组件语义断裂——--dark 描述的是系统偏好,不是按钮自身的状态(如 --disabled 或 --loading)。结果是所有组件都要为每个主题重复写一遍修饰符,变量无法联动,PurgeCSS 极易误删未在首屏渲染的类,SSR 时还可能白屏。
html[data-theme="dark"] 是唯一安全的根级控制方式
主题切换必须由 html 元素上的属性驱动,而不是 body 或自定义容器。原因很实际:
-
body--dark违反 BEM 契约——body不是你定义的 Block -
window.matchMedia触发时,body类不会自动同步,漏监听就失焦 - PurgeCSS 默认只保留 HTML 中显式出现的 class,首次访问亮色模式时
body--dark样式大概率被删 - 原生表单控件(如
<select>)在深色系统下本应自动适配,但body--dark input强制覆盖反而破坏可访问性
正确做法是:document.documentElement.dataset.theme = 'dark',配合 CSS 中的 html[data-theme="dark"] .button--primary 重定义变量。
变量命名要带 BEM 思维,不是随便拼 --color-text-primary
变量名得体现「作用域 + 类型 + 含义」三层结构,和 BEM 类名形成映射:
立即学习“前端免费学习笔记(深入)”;
-
--color-text-primary:其中color对应 Block(颜色模块),text是 Element(文本用途),primary是 Modifier(主要状态) -
--space-unit-sm、--radius-card同理,IDE 补全有效,DevTools 过滤清晰 - 组件内不直接写颜色值,只声明角色:
.button--primary表示“这是主按钮”,外观全靠var(--btn-bg)驱动 - 所有变量必须带 fallback:
background-color: var(--btn-bg, #007bff);,否则 JS 加载失败或 SSR 首屏会白屏
组件如何响应主题而不污染结构
BEM 修饰符只管“我是什么”,不管“当前环境什么样”。所以:
- 禁止组合类名:
.button--primary.button--dark是错的——它把状态和主题耦合,破坏正交性 - 别在组件内部硬写
html[data-theme="dark"] & { ... }嵌套,容易导致选择器权重失控,后续加.button--loading时样式被压制 - 多主题共存(如深色+高对比度)时,用叠加修饰符:
app app--theme-dark app--theme-high-contrast,而非拼成app--theme-dark-high-contrast - 嵌套子元素(如
.card__header)需要的颜色,由.card根节点统一声明变量并靠继承透传,不要为每个子元素单独造变量前缀
真正难的不是写法,而是变量覆盖的范围是否完整——漏掉 --card-bg 而只改了 --text-color,就会出现卡片背景还是亮色、文字却变黑的诡异情况。


















