BEM(Block Element Modifier)命名规范在实际开发中衍生出多种变体。这些变体并非刻意标新立异,而是为了适应现代前端开发中的各种限制——例如 React 组件的粒度、CSS-in-JS 的写法、构建产物体积的控制,以及团队自身的编码习惯。双连字符写法(--)语义清晰,能与 __ 形成视觉上的对称,并且在 SCSS 中书写 &--hover 嵌套非常顺手;单连字符写法(-)压缩后体积更优,但容易与 Block 名中的连字符产生混淆。UI 库添加前缀(比如 el-button)是为了防止全局污染,同时支持多版本共存;而业务组件通常不需要这样做。

为什么会有 BEM 变体,而不是统一用 .block__element--modifier
BEM 原始规范由 Yandex 提出,但在实际项目中几乎没有人会 100% 照搬。根本原因在于:原生 BEM 对长类名容忍度很高,对工具链也没有硬性要求;但现代前端环境完全不同——既要考虑 React 组件的颗粒度,又要兼顾 CSS-in-JS 的写法,还得控制构建产物体积、维持可读性,以及照顾团队习惯。因此,变体的出现并非“不守规矩”,恰恰是为了解决这些具体约束所做的权衡与适配。
double-dash 和 single-dash 修饰符写法怎么选
标准写法是 .block--modifier(双连字符),但有些团队改用 .block-modifier(单连字符)。这不是随意的缩写,背后有明确的取舍:
- 用
--:语义清晰,与__形成视觉对称,而且 CSS 预处理器(比如 SCSS)能安全地嵌套&--hover;但类名字符更长,即便压缩后仍然显眼。 - 用
-:能缩短类名(比如.btn-primary比.btn--primary少 2 个字节),适合那些对 bundle size 特别敏感的项目;但容易与 Block 名中的连字符混淆,例如看到.user-profile-active就难以判断profile究竟是 Block 还是 Element。 - 最危险的是混用:如果同时出现
.btn--large和.btn-primary,开发者就需要反复查阅文档才能确认正确写法,效率大打折扣。
带命名空间前缀的 BEM(如 el-button)适用什么场景
像 Element Plus、Ant Design 这类 UI 库普遍采用 [prefix]-[block] 格式(比如 el-button、ant-table)。这不是 BEM 的“错误用法”,而是封装隔离的刚需:
- 避免全局类名污染:UI 库被多个项目共用时,
button这个名字太通用,el-button则明确多了,一眼能看出归属。 - 支持多主题/多版本共存:
el-button和el-button-v2可以同时加载而不会冲突;但若使用.button--v2,就会与旧版样式搅在一起。 - 不过,业务组件一般不需要这么折腾。你自己写的
user-card再加个my-user-card前缀,反而增加了认知负担,除非是跨团队共享的组件库。
React 中 classnames + BEM 的常见错配点
很多人以为安装了 classnames 就算用了 BEM,其实关键在 Block 名是否收敛:
- 错误写法:
className={cx('card__header', { 'card__header--expanded': expanded })}——Block 名硬编码在 JSX 里,一旦组件重命名或复用,类名就全部崩坏。 - 正确写法:在顶部定义
const BLOCK = 'user-profile',然后统一用cx(`${BLOCK}__header`, { [`${BLOCK}__header--expanded`]: expanded })。 - 更进一步:可以抽成 hook,比如
useBem('user-profile')返回getBlock、getElement方法,彻底避免字符串拼接出错。
真正困难的其实不是记住 __ 和 -- 的区别,而是在每次新增 class 时,同步判断它到底该属于 Block、Element 还是 Modifier——这个决策一旦出错,后续所有样式维护都会变成“修修补补式”的噩梦。
