根本原因是文件名、类名、JSX访问键三者未对齐:文件名须PascalCase且与组件名一致(如Button.tsx↔Button.module.css),类名须以.button__element形式声明,JSX中必须用styles['button__icon']访问,不可用点号或硬编码字符串。

React 18 组件中 BEM 类名不稳定,90% 是因为文件名、类名、JSX 访问三者没对齐,不是 CSS 写错了,也不是 React 版本问题。
为什么 styles.button__icon 总是 undefined
根本原因不是 CSS 没写,而是 CSS Modules 的哈希前缀生成逻辑和 BEM 的 Block 名强绑定:文件叫 Button.module.css → 编译后类名前缀是 Button_button__xxx;如果文件是 button.module.css 或 MyButton.module.css,styles.button__icon 就永远查不到。
- 文件名必须 PascalCase,且与组件名完全一致(
Button.tsx↔Button.module.css) - CSS 中所有类名必须以
.button开头(禁止.btn、.ui-button) - Element 必须用双下划线:
.button__icon✅,.button-icon❌(后者不参与 BEM 映射) - JSX 中必须用字符串键访问:
styles['button__icon']✅,styles.button__icon❌(点号访问在 TS/JS 中非法)
如何安全拼接动态类名:别手写字符串,用 bem() 工厂 + classnames
手写 {`button ${isDisabled ? 'button--disabled' : ''}`} 看似快,实则埋雷:空格漏写、连字符错位(button-disabled)、isDisabled 为 null 时留下多余空白,更关键的是绕过类型检查和 CSS Modules 校验。
- 封装一次
bem('button'),得到稳定引用:const buttonBem = bem('button') - 所有类名都派生自它:
buttonBem()、buttonBem.e('icon')、buttonBem.m('primary') - 传给
classnames的值必须全来自buttonBem:cn(styles[buttonBem()], { [styles[buttonBem.m('disabled')]]: disabled }) - 禁止混入字面量:
cn('button', { 'button--disabled': disabled })❌ —— 构建期无校验,TS 无法约束
Modifier 值为什么必须受控枚举?
button--${variant} 这种插值看似灵活,但 variant="prmiary" 拼错、variant=undefined 或 variant="javascript:alert(1)" 都不会报错,只会静默失效或注入非法类名。
立即学习“前端免费学习笔记(深入)”;
- Modifier 只表达有限、可枚举的状态,不是任意字符串容器
- TypeScript 下可加泛型约束:
type ButtonVariant = 'primary' | 'secondary',配合bem<ButtonVariant>('button') - 禁止用 Modifier 表达位置或上下文(如
button--in-modal),那是结构职责,该由父组件 wrapper 或独立 Block 承担 - 多个 Modifier 平级组合:
button--primary button--large button--loading,而非button--primary-large-loading
构建期如何守住 BEM 边界?postcss-bem-linter 不是可选,是底线
没有它,团队会自然滑向反模式:在 Button.module.css 里偷偷写 .modal__close,或允许 .button__icon--large--dark 这种嵌套 Modifier,破坏 Block 封装性。
- 它能拦截:
.button .icon(带空格的后代选择器)、.button-icon(单下划线)、.button__content--loading(Element 下挂 Modifier) - 配合 ESLint 插件
eslint-plugin-css-modules,可校验 JSX 中 className 字符串是否匹配当前 CSS 文件声明的 Block 前缀 - 开发期配置
localIdentName=[name]__[local]___[hash:base64:5],一眼看出类名来源(如Button_button__abc123)
真正难的不是写对一个 .card__header,而是在组件重渲染、props 变化、多人协作、第三方样式介入时,让每个类名的生成路径始终可追溯、可验证、不可绕过——这需要工厂函数固化 Block、工具链强制校验、团队约定落地到 lint 规则,而不是靠记忆或自觉。


















