css modules与bem必须协同而非替代:文件名(pascalcase)、类名(.block__element)、jsx访问键(styles['block__element'])三者须严格一致,缺一不可;否则styles.button__icon必为undefined。

React中CSS Modules与BEM的映射必须对齐文件名、类名、JSX键
styles.button__icon返回undefined,90%不是CSS写错了,而是Button.module.css文件名和组件名不一致,或类名没以.button开头。CSS Modules用文件名生成哈希前缀:Button.module.css → Button_button__xxx;如果文件叫button.module.css或MyButton.module.css,styles.button__icon永远查不到。
必须满足三者严格一致:
- 文件名 PascalCase,且与组件名完全相同(
Card.tsx↔Card.module.css) - CSS中所有类名必须以
.card开头(禁止.product-card或.ui-card) - Element必须用双下划线:
.card__header✅,.card-header❌(后者不参与BEM映射) - JSX中访问必须用字符串键:
styles['card__header']✅,styles.card__header❌(点号非法)
验证方式:编译后看CSS类名是否以Card_开头;再console.log(styles)确认键是否存在。
动态拼接BEM类名必须用classnames + BEM工厂函数
手写{`button ${isDisabled ? 'button--disabled' : ''}`}是高危操作:空格漏写、拼错--、传入null导致多余空白,而且绕过CSS Modules校验,样式静默失效。
正确做法是封装BEM工厂:
const bem = (block) => ({
e: (el) => `${block}__${el}`,
m: (mod) => `${block}--${mod}`
});
const buttonBem = bem('button');
再配合classnames使用:
className={cn(
styles[buttonBem()],
{ [styles[buttonBem.m('primary')]]: isPrimary },
{ [styles[buttonBem.e('icon')]]: hasIcon }
)}
禁止混用字符串字面量:cn('button', { 'button--disabled': disabled })❌——绕过类型检查,TS无法约束,构建时也无校验。
postcss-bem-linter不是可选插件,是防止结构滑坡的底线
没有它,团队很快会写出.button__icon--large--dark(嵌套修饰符)、.button-icon(缺双下划线)、.button .icon(后代选择器)这类反模式。它在构建阶段强制拦截:
-
.button__content--loading:Element下挂Modifier,违反BEM语义 -
.modal__close在Button.module.css里出现:跨Block引用,破坏封装边界 -
.button-icon:缺少__,不被识别为Element
它不保证样式美观,只守住“Block边界不被突破”这一条线。
BEM块名必须是语义化、可复用的独立单元,不是HTML标签或容器
.header、.section、.wrapper都不是合法Block名——它们是泛布局容器,一改结构就得重写全部CSS。真正有效的Block名要带业务含义且脱离上下文仍可理解,比如.search-form、.user-card。
子组件是否该独立成Block?判断标准很实在:删掉父级,它还能不能单独存在、复用?
-
.profile-card__avatar合理:头像离开卡片通常无独立语义 -
.logo应独立:页头、页脚、弹窗都用同一套样式,它自己就是Block
Element命名只允许一层:.card__title__link❌,.card__title-link也不对——如果需要链接,要么它是.card__title的子元素(用.card__title-link),要么它属于另一个Block(如.link)。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











