直接拼接BEM类名易出错,应封装bem工厂函数固定block名并派生element/modifier;配合classNames时须全用函数生成值,禁用手写字符串或插值;TypeScript可加泛型约束modifier值;CSS Modules需映射样式对象。

直接用 classNames 拼接 BEM 类名会出问题——不是库不行,是没把 block 作用域封住,button__icon--loading 和 button--loading 语义不同,但字符串里根本分不清。
为什么不能直接写 className={`button button--${variant} ${size ? `button--${size}` : ''}`}
这种写法看着快,实际踩三个坑:
-
button--${variant}中漏空格或连字符错位(比如写成button-${variant})会导致样式完全不生效 -
variant="prmiary"这种拼写错误,TypeScript 不报错、运行时无提示,只有样式“消失”这一个症状 - 无法约束结构:你没法阻止开发者把
button__label--error写成button--error,而二者在 CSS 里是完全独立的选择器
必须封装一个 bem 工厂函数
核心是固定 block 名,再由它派生 element 和 modifier,让所有类名生成路径可控。例如:
const buttonBem = bem('button');
// 生成 button--primary
buttonBem.modifier('primary')
// 生成 button__icon
buttonBem.element('icon')
// 生成 button__icon--loading
buttonBem.element('icon').modifier('loading')
这个 bem 函数本身不依赖 classNames,但它输出的每个值都必须是字符串,才能安全传给 classNames。
搭配 classNames 的正确姿势
传给 classNames 的参数,必须全部来自封装后的 BEM 函数,不能混入手写字符串:
- ✅ 正确:
classNames(buttonBem(), { [buttonBem.modifier('disabled')]: disabled }) - ❌ 错误:
classNames('button', { 'button--disabled': disabled })—— 手写字符串绕过校验 - ⚠️ 危险:
classNames(buttonBem(), `button__icon--${status}`)—— 字符串插值破坏类型和结构约束
如果你用 TypeScript,bem 函数可以加泛型约束 modifier 可选值(如 type ButtonModifier = 'primary' | 'secondary'),这样拼错就直接编译报错。
CSS Modules 场景下还要多一步映射
如果项目用的是 button.module.css,那 bem 生成的字符串只是“原始名”,必须通过 styles 对象转成真实哈希类名:
import styles from './button.module.css';
const buttonBem = bem('button', (name) => styles[name]); // 第二个参数是转换器
// 最终渲染:
<button className={classNames(
buttonBem(),
{ [buttonBem.modifier('primary')]: true }
)} />
这里容易忽略的是:所有可能用到的 modifier、element 都必须提前定义在 CSS 文件中,否则 styles['button--primary'] 是 undefined,最终类名为空。


















