@at-root #{&}__element是唯一安全生成BEM类名的方式,纯嵌套会产出带空格的后代选择器,违背扁平化原则;因&代表完整父选择器字符串,直接&__title编译为.card .card__title,而BEM要求无空格单类名.card__title。
直接说结论:SCSS 的父选择器 & 是生成 BEM 类名最自然的工具,但必须配合字符串拼接(#{})和严格嵌套结构,否则容易产出错误层级或重复类名。
常见错误是写成这样:
.card {&__header { color: #333; }&__body { padding: 1rem; }}
这确实能编译出 .card__header 和 .card__body,但一旦加了伪类、媒体查询或嵌套子元素,& 就会带出完整路径,比如:
.card {&:hover {&__header { /* 编译为 .card:hover__header —— 错了! */ }}}
正确做法是把修饰符/状态/元素都作为独立层级处理:
__element 必须直接挂在 block 下,不嵌套在 :hover 或 .card-content 之类内部&.card--active 而不是 &:active(后者是伪类,BEM 不鼓励用伪类作修饰符).card--active &__header,而不是靠 & 自动拼接当需要动态生成多个 BEM 元素(比如循环一组 icon 类型),& 无法满足,必须用 #{} 插值:
$icons: (home, search, user);.card {@each $icon in $icons {&__icon--#{$icon} {background: url('icon-#{$icon}.svg');}}}
这里 &__icon--#{$icon} 中的 & 展开为 .card,再拼上固定字符串和变量,结果是 .card__icon--home 等。关键点:
& 只能代表当前选择器前缀,不能“截断”或“重置”,所以别指望 &__item:hover 在嵌套里还能干净输出#{} 内部不能含空格或非法字符,$name 建议用 kebab-case 验证&,比如 #{&}__bad —— SCSS 不允许,会报错 Invalid CSS
BEM 要求修饰符与 block 同级(.card--featured),但 SCSS 嵌套时容易误写成子类:
.card {&--featured { /* 正确:编译为 .card--featured */ }&__content {&--compact { /* 错误:编译为 .card__content--compact,这不是标准 BEM 修饰符位置 */ }}}
真正合规的做法是把修饰符提一层,或用 @at-root 跳出嵌套:
.card {&--featured {@at-root .card--featured &__header {font-weight: bold;}}}
这样 .card--featured .card__header 才符合 BEM 规范 —— 修饰符作用于 block,再影响其内部元素。实际项目中更推荐直接写平级规则,避免过度依赖 @at-root。
有人封装 @mixin bem($block) 来统一生成 __ 和 --,但多数场景反而增加复杂度:
$block)会导致编译出 .__element 这种非法类名除非团队有强自动化需求(比如对接 Design Token 系统),否则手写 &__xxx + 人工校验,比引入 mixin 更可控。
BEM 类名生成看着简单,但每个 & 的展开时机、每个 #{} 的上下文、每个修饰符的挂载位置,都会在编译后暴露出来——它不报错,但会悄悄破坏命名一致性。写的时候多看一眼编译结果,比背熟语法更重要。