核心是用.block__element替代.block .element:后者依赖dom层级,结构一变即失效;前者通过显式类名解耦,改html仍稳定生效。

降低 CSS 选择器对 DOM 结构的依赖,核心不是“少写几层”,而是用 .block__element 替代 .block .element —— 后者一改结构就失效,前者改完 HTML 仍稳如磐石。
为什么 .card .title 比 .card__title 更容易挂
浏览器匹配 .card .title 是从右往左:先找所有 .title,再逐个往上查父级有没有 .card。人眼却从左往右读,调试时根本想不到是“.title 找多了”还是“.card 少了一层”。更麻烦的是:
- 你把
<h2 class="title"></h2>换成<div class="title">?样式照常生效,但语义崩了,JS 也可能误判 <li>第三方组件(比如 <code>react-datepicker)自带.input,塞进.card里立刻被.card .input覆盖 -
.card .title在 DevTools 里不报错、不警告,静默失效——你得靠肉眼翻 DOM 才能发现哪段没生效 - 每个
element必须属于且只属于一个block:.card__title合法,.card__header__title违规(Element 不嵌 Element) - 禁用任何空格分隔的选择器:
.card .card__title是典型自毁行为,等于又套了一层结构依赖 - Sass 中慎用嵌套:
.card { &__content { .item { ... } } }编译后就是.card__content .item,不是.card__content__item,但依然违反 BEM 原则
如何正确写出 BEM 类名并避免嵌套陷阱
BEM 不是加长类名,是把作用域“写进名字里”。关键动作有三步:
修饰符(--)怎么用才不翻车
-- 不是拼接后缀,是表达正交状态。比如按钮有“主题”和“禁用”两个维度,应写成:
.button { background: #fff; }
.button--primary { background: #007bff; }
.button--disabled { opacity: 0.5; pointer-events: none; }
这样 class="button button--primary button--disabled" 才可预测、可单独移除。常见错误包括:
- 写成
.button--primary-disabled:状态耦合,无法动态开关“禁用”而不影响“主题” - 用伪类覆盖修饰符:
.button--disabled:hover—— 鼠标悬停时禁用态失效,违背交互逻辑 - 修饰符作用到子元素上:
.card--featured .card__title→ 应改为.card__title--featured或提取新 block
老项目怎么过渡,又不推倒重来
直接全局替换不现实,但可以分阶段收口:
- 新功能/新组件强制用 BEM,旧区域暂时冻结,禁止新增
.a .b类型规则 - 用
[data-bem="card"] .title替代.card .title,属性选择器比后代选择器更可控,也便于后续 grep 清理 - 在 CI 中接入
stylelint-selector-bem-pattern并开 strict 模式,自动拦截.block .element和.block > .element - JS 操作类名时别硬编码:
el.classList.toggle('card__item--active')比el.classList.toggle('active')更安全,也比拼字符串强
真正难的不是命名规则本身,而是所有人对“这个类名代表什么作用域”达成一致。一旦有人偷偷写 .modal .close-btn,整套解耦就从内部瓦解——BEM 的边界感,得靠工具兜底、靠代码审查守住。











