BEM(块元素修饰符)是一种流行的 CSS 类命名约定,它使 CSS 更易于维护。本文假设您已经熟悉此命名约定。如果您不熟悉,可以访问 getbem.com 了解更多信息,了解基本知识。
BEM 的标准语法是
block-name__element-name--modifier-name
我个人非常喜欢这种命名约定背后的方法论。将样式分解成小的组件比在整个样式表中使用大量高特异性的样式更容易维护。但是,我对语法有一些问题,这些问题可能会导致生产问题,并给开发人员带来困惑。我更喜欢使用稍微调整后的语法版本。我称之为 ABEM(原子块元素修饰符)
[a/m/o]-blockName__elementName -modifierName
原子设计前缀
a/m/o 是一个 原子设计 前缀。不要与 原子 CSS 混淆,后者是完全不同的东西。原子设计是一种组织组件的方法,它最大限度地提高了代码重用能力。它将组件分成三个文件夹:原子、分子和有机体。原子是超简单的组件,通常仅由单个元素组成(例如,按钮组件)。分子是元素和/或组件的小组(例如,显示标签和输入字段的单个表单字段)。有机体是由许多分子和原子组件组成的复杂组件(例如,完整的注册表单)。

使用经典 BEM 进行原子设计时遇到的困难在于,没有指示器表明块是什么类型的组件。这可能难以知道该组件的代码在哪里,因为您可能需要在 3 个单独的文件夹中搜索才能找到它。在开头添加原子前缀可以立即清楚地表明该组件存储在哪个文件夹中。
驼峰式命名法
允许自定义分组
经典 BEM 使用单个连字符分隔每个部分中的各个单词。请注意,上面示例中的原子前缀也通过连字符与类名的其余部分分隔。请查看现在将原子前缀添加到经典 BEM 与驼峰式命名法时会发生什么情况。
/* classic + atomic prefix */
.o-subscribe-form__field-item {}
/* camelCase + atomic prefix */
.o-subscribeForm__fieldItem {}
乍一看,使用经典方法读取时,组件名称看起来像是“o subscribe form”。“o”的意义完全丢失了。但是,当您将“o-”应用于驼峰式命名法版本时,很明显它是故意将其写成与组件名称分开的独立信息。
现在,您可以通过将“o”大写来将原子前缀应用于经典 BEM,如下所示:
/* classic + capitalized atomic prefix */
.O-subscribe-form__field-item {}
这将解决“o”在类名其余部分中丢失的问题,但它没有解决经典 BEM 语法中的核心底层问题。通过使用连字符分隔单词,连字符不再可用作分组机制。通过使用驼峰式命名法,它使您可以自由地使用连字符进行其他分组,即使该分组只是在类名末尾添加一个数字。
您的思维将更快地处理分组
驼峰式命名法还有一个额外的好处,那就是使类名的分组更容易在心理上处理。使用驼峰式命名法,您在类名中看到的每个间隙都表示某种分组。在经典 BEM 中,每个间隙可能是分组或同一组中两个单词之间的空格。
请查看经典 BEM 类(加上原子前缀)的轮廓,并尝试找出前缀、块、元素和修饰符部分的开始和结束位置。
好的,现在试试这个。它与上面的类完全相同,只是这次它使用驼峰式命名法而不是连字符来分隔每个单词。
是不是容易多了?这些轮廓本质上是您在扫描代码时脑海中看到的。类名中所有这些额外的连字符使分组变得不那么清晰。当您阅读代码时,您的大脑会尝试处理它遇到的间隙是新的分组还是仅仅是新单词。这种缺乏清晰度会导致认知负荷在您工作时加重您的思维。
经典 BEM + 原子前缀
驼峰式命名法 BEM + 原子前缀
谨慎使用多类选择器
BEM 的黄金法则之一是每个选择器只应该包含一个类。其理念是通过保持选择器的特异性较低且易于管理来保持 CSS 的可维护性。一方面,我同意低特异性比特异性泛滥更可取。另一方面,我强烈不同意每个选择器严格使用一个类的规则是项目中最好的选择。在您的样式中使用一些多类选择器实际上可以提高可维护性,而不是降低它。
“但是它会导致更高的特异性!难道你不知道特异性本质上是邪恶的吗??”
特异性 != 坏。
不受控制的特异性失控 = 坏。
拥有某些较高特异性的声明并不立即意味着您的 CSS 难以维护。如果以正确的方式使用,赋予某些规则更高的特异性实际上可以使 CSS 更易于维护。编写具有不均匀特异性的可维护 CSS 的关键是有目的地添加特异性,而不仅仅是因为列表项碰巧位于列表元素内部。
此外,难道我们不希望我们的修饰符样式比默认样式对元素具有更大的控制力吗?为了保持修饰符样式与普通样式具有相同的特异性级别而反其道而行之,在我看来很愚蠢。您什么时候真正希望您的常规默认样式覆盖您专门指定的修饰符样式?
分离修饰符可以使 HTML 更简洁
这是 ABEM 引入的语法方面最大的变化。您不是将修饰符连接到元素类,而是将其作为单独的类应用。
当人们第一次开始学习 BEM 时,几乎每个人都会抱怨它有多丑。在涉及修饰符时尤其如此。请查看这种暴行。它只应用了三个修饰符,但看起来却像一场火车事故。
B__E–M
<button class="block-name__element-name block-name__element-name--small block-name__element-name--green block-name__element-name--active">
Submit
</button>
看看所有这些重复!这种重复使得很难阅读它实际上想要做什么。现在,请查看此 ABEM 示例,它与上一个示例具有相同的修饰符。
A-B__E -M
<button class="a-blockName__elementName -small -green -active">
Submit
</button>
是不是干净多了?去掉所有重复的冗余信息后,更容易理解这些修饰符类想要表达的意思。
在使用浏览器开发者工具检查元素时,你仍然可以在样式面板中看到完整的规则,因此它以这种方式保留了与原始组件的连接。
.a-blockName__elementName.-green {
background: green;
color: white;
}
它与BEM等价物没有太大区别。
.block-name__element-name--green {
background: green;
color: white;
}
管理状态变得容易
ABEM相较于经典BEM的一个巨大优势在于,它使得管理组件状态变得异常容易。让我们以一个基本的折叠面板为例。当折叠面板的某个部分打开时,假设我们希望对样式进行以下更改:
- 更改部分标题的背景颜色。
- 显示内容区域。
- 将向下箭头改为向上箭头。
在本例中,我们将坚持使用经典的B__E–M语法,并严格遵守每个CSS选择器一个类的规则。最终得到的结果如下(注意,为了简洁起见,此折叠面板不可访问)。
查看CodePen上的示例Accordion 1 – Pure BEM,作者是Daniel Tonon (@daniel-tonon),发布在CodePen上。
SCSS看起来很简洁,但请查看一下,仅仅为了改变一个状态,我们在HTML中需要添加多少额外的类!
使用BEM时HTML片段处于关闭状态
<div class="revealer accordion__section">
<div class="revealer__trigger">
<h2 class="revealer__heading">Three</h2>
<div class="revealer__icon"></div>
</div>
<div class="revealer__content">
Lorem ipsum dolor sit amet...
</div>
</div>
使用BEM时HTML片段处于打开状态
<div class="revealer accordion__section">
<div class="revealer__trigger revealer__trigger--open">
<h2 class="revealer__heading">One</h2>
<div class="revealer__icon revealer__icon--open"></div>
</div>
<div class="revealer__content revealer__content--open">
Lorem ipsum dolor sit amet...
</div>
</div>
现在让我们看看当我们切换到使用这种新奇的A-B__E -M方法时会发生什么。
查看CodePen上的示例Accordion 2 – ABEM alternative,作者是Daniel Tonon (@daniel-tonon),发布在CodePen上。
现在,单个类控制整个组件的状态特定样式,而不是必须分别对每个元素应用单独的类。
使用ABEM时HTML片段处于打开状态
<div class="m-revealer o-accordion__section -open">
<div class="m-revealer__trigger">
<h2 class="m-revealer__heading">One</h2>
<div class="m-revealer__icon"></div>
</div>
<div class="m-revealer__content">
Lorem ipsum dolor sit amet...
</div>
</div>
此外,请注意JavaScript变得多么简单。我尽可能简洁地编写了JavaScript,结果如下:
使用纯BEM时的JavaScript代码
class revealer {
constructor(el){
Object.assign(this, {
$wrapper: el,
targets: ['trigger', 'icon', 'content'],
isOpen: false,
});
this.gather_elements();
this.$trigger.onclick = ()=> this.toggle();
}
gather_elements(){
const keys = this.targets.map(selector => `$${selector}`);
const elements = this.targets.map(selector => {
return this.$wrapper.querySelector(`.revealer__${selector}`);
});
let elObject = {};
keys.forEach((key, i) => {
elObject[key] = elements[i];
});
Object.assign(this, elObject);
}
toggle(){
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open(){
this.targets.forEach(target => {
this[`$${target}`].classList.add(`revealer__${target}--open`);
})
this.isOpen = true;
}
close(){
this.targets.forEach(target => {
this[`$${target}`].classList.remove(`revealer__${target}--open`);
})
this.isOpen = false;
}
}
document.querySelectorAll('.revealer').forEach(el => {
new revealer(el);
})
使用ABEM时的JavaScript代码
class revealer {
constructor(el){
Object.assign(this, {
$wrapper: el,
isOpen: false,
});
this.$trigger = this.$wrapper.querySelector('.m-revealer__trigger');
this.$trigger.onclick = ()=> this.toggle();
}
toggle(){
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
open(){
this.$wrapper.classList.add(`-open`);
this.isOpen = true;
}
close(){
this.$wrapper.classList.remove(`-open`);
this.isOpen = false;
}
}
document.querySelectorAll('.m-revealer').forEach(el => {
new revealer(el);
})
这只是一个非常简单的折叠面板示例。想象一下,当您将其扩展到诸如粘性头部(在粘性时会发生变化)之类的场景时会发生什么。粘性头部可能需要在头部变为粘性时通知5个不同的组件。然后,在每个组件中,可能有5个元素需要对头部变为粘性做出反应。这意味着我们需要在js中编写25个element.classList.add("[componentName]__[elementName]--sticky")
规则才能严格遵守BEM命名约定。哪种方法更有意义?是向每个受影响的元素添加25个唯一的类,还是只向头部添加一个-sticky
类,所有5个组件中的5个元素都能够轻松访问和读取?
BEM的“解决方案”完全不切实际。将修饰符样式应用于大型复杂组件最终会变成一个灰色地带。这个灰色地带会让任何试图尽可能严格遵守BEM命名约定的开发者感到困惑。
ABEM修饰符问题
分离修饰符并非没有缺点。但是,有一些简单的方法可以解决这些缺点。
问题1:嵌套
所以我们有了折叠面板,并且一切正常。稍后,客户希望在第一个折叠面板内嵌套第二个折叠面板。所以你继续这样做……然后发生了这种情况。
查看CodePen上的示例Accordion 3 – ABEM nesting bug,作者是Daniel Tonon (@daniel-tonon),发布在CodePen上。
在第一个折叠面板内嵌套第二个折叠面板会导致一个相当棘手的问题。打开父折叠面板也会将打开状态的样式应用于该片段中所有子折叠面板。
这显然是不希望发生的。但是,有一个很好的方法可以避免这种情况。
为了解释清楚,让我们玩一个小游戏。假设这两个CSS规则都作用于同一个元素,您认为该元素的背景色会是什么颜色?
.-green > * > * > * > * > * > .element {
background: green;
}
.element.-blue {
background: blue;
}
如果您说由于第一个规则的特殊性高于第二个规则,所以是绿色,那么您实际上是错的。它的背景色将是蓝色。
有趣的事实:*
是CSS中最低的特殊性选择器。它基本上表示CSS中的“任何东西”。它实际上没有特殊性,这意味着它不会为添加到其中的选择器增加任何特殊性。这意味着,即使您使用了一个包含单个类和5个星号的规则(.element > * > * > * > * > *
),它仍然可以被下一行CSS中的单个类轻松覆盖!
我们可以利用这个CSS的小技巧来为折叠面板的SCSS代码创建更具针对性的方法。这将允许我们安全地嵌套折叠面板。
查看CodePen上的示例Accordion 4 – ABEM nesting bug fix,作者是Daniel Tonon (@daniel-tonon),发布在CodePen上。
通过使用.-modifierName > * > &
模式,您可以定位多层深的直接子元素,而不会导致特殊性失控。
我仅在必要时才使用这种直接定位技术。默认情况下,当我编写ABEM时,我会像在最初的ABEM折叠面板示例中那样编写。在大多数情况下,非目标方法通常就足够了。目标方法的问题在于,在某些内容周围添加单个包装器可能会破坏整个系统。非目标方法不会遇到此问题。它更加宽松,并且可以防止在以后需要修改HTML时样式中断。
问题2:命名冲突
使用非目标修饰符技术时可能会遇到一个问题,那就是命名冲突。假设您需要创建一组选项卡,并且每个选项卡中都有一个折叠面板。在编写此代码时,您使折叠面板和选项卡都对-active
类做出响应。这会导致命名冲突。活动选项卡中的所有折叠面板都将应用其活动样式。这是因为所有折叠面板都是选项卡容器元素的子元素。选项卡容器元素才是实际应用了-active
类的元素。(以下示例中的选项卡和折叠面板均不可访问,以简洁起见)。
查看CodePen上的示例Accordion in tabs 1 – broken,作者是Daniel Tonon (@daniel-tonon),发布在CodePen上。
解决此冲突的一种方法是简单地更改手风琴,使其响应-open
类而不是-active
类。我实际上建议采用这种方法。不过,为了举例说明,假设这不是一种选择。您可以使用上面提到的直接目标技术,但这会使您的样式非常脆弱。相反,您可以这样做,在修饰符的前面添加组件名称,如下所示
.o-componentName {
&__elementName {
.-componentName--modifierName & {
/* modifier styles go here */
}
}
}
名称前面的破折号仍然表示它是一个修饰符类。组件名称可防止与不应受到影响的其他组件发生命名空间冲突。双破折号主要是向经典的 BEM 修饰符语法致敬,以双重强调它是一个修饰符类。
以下是手风琴和选项卡示例,但这次应用了命名空间修复
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Accordion in tabs 2 – fixed。
但是,我建议不要默认使用此技术,主要是为了保持 HTML 的简洁性,并避免在多个组件需要共享相同的修饰符时造成混淆。
在大多数情况下,修饰符类用于表示状态的变化,如上例中的手风琴。当元素状态发生变化时,所有子元素(无论属于哪个组件)都应该能够读取该状态变化并轻松响应它。当修饰符类旨在同时影响多个组件时,可能会对该修饰符具体属于哪个组件产生混淆。在这些情况下,对修饰符进行命名空间处理弊大于利。
ABEM 修饰符技术总结
因此,为了最大程度地利用 ABEM 修饰符,请默认使用.-modifierName &
或&.-modifierName
语法(取决于哪个元素具有该类)。
.o-componentName {
&.-modifierName {
/* componentName modifier styles go here */
}
&__elementName {
.-modifierName & {
/* elementName modifier styles go here */
}
}
}
如果将组件嵌套到自身内部导致问题,请使用直接目标定位。
.o-componentName {
&__elementName {
.-nestedModifierName > * > & {
/* modifier styles go here */
}
}
}
如果遇到共享修饰符名称冲突,请在修饰符中使用组件名称。仅当您无法想到其他仍然有意义的修饰符名称时,才执行此操作。
.o-componentName {
&__elementName {
.-componentName--sharedModifierName & {
/* modifier styles go here */
}
}
}
上下文相关的样式
严格遵守 BEM 每选择器一个类的 методология 的另一个问题是它不允许您编写上下文相关的样式。
上下文相关的样式基本上是“如果此元素位于此父元素内部,则将其应用于此元素”。
对于上下文相关的样式,存在一个父组件和一个子组件。父组件应负责为子组件应用布局相关的样式,例如边距和位置(.parent .child { margin: 20px }
)。默认情况下,子组件永远不应该在其组件外部有任何边距。这允许子组件在更多上下文中使用,因为是父组件负责其自己的布局,而不是其子组件。
就像真正的育儿一样,父母应该负责。在涉及父母的布局时,你不应该让他们的淘气无知的子女来做决定。
为了更深入地了解这个概念,让我们假设我们正在构建一个全新的网站,现在我们正在为该网站构建订阅表单组件。
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 1 – IE unfriendly。
这是我们第一次必须在正在构建的这个很棒的新网站上放置表单。我们想成为所有酷孩子中的一员,所以我们使用CSS 网格来进行布局。不过我们很聪明。我们知道按钮样式将在网站的更多地方使用。为了做好准备,我们像优秀的开发人员一样,将订阅按钮样式分离到其自己的单独组件中。
过了一段时间,我们开始进行跨浏览器测试。我们打开 IE11,却发现这个丑陋的东西正盯着我们。

IE11 确实有点支持 CSS 网格,但它不支持grid-gap
或自动放置。在经历了一些发泄式的咒骂并希望人们更新他们的浏览器后,您调整了样式,使其看起来更像这样
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 2 – what not to do。
现在它在 IE 中看起来很完美。世界一切安好。还能出什么问题呢?
几个小时后,您将此按钮组件放入网站上的另一个组件中。此其他组件也使用 css-grid 来布局其子元素。
您编写了以下代码
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 3 – the other component。
您希望看到一个即使在 IE11 中也看起来像这样的布局

但实际上,由于您之前编写的grid-column: 3;
代码,它最终看起来像这样

哎呀!那么我们如何处理之前编写的grid-column: 3;
CSS 呢?我们需要将其限制在父组件中,但我们应该如何进行呢?
好吧,处理此问题的经典 BEM 方法是像这样向按钮添加一个新的父组件元素类
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 4 – classic BEM solution。
表面上,此解决方案看起来还不错
- 它保持了较低的特异性
- 父组件控制其自己的布局
- 样式不太可能泄漏到我们不希望它泄漏到的其他组件中
一切都很棒,世界一切安好……对吧?
这种方法的缺点主要是由于我们必须向按钮组件添加一个额外的类。由于subscribe-form__submit
类在基本button
组件中不存在,这意味着我们需要向用作模板引擎的任何内容添加额外的逻辑,以便它接收正确的样式。
我喜欢使用Pug来生成我的页面模板。我将以 Pug 混合为例向您展示我的意思。
首先,这是以混合格式重写的原始 IE 不友好代码
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 5 – IE unfriendly with mixins。
现在让我们向其中添加 IE 11 subscribe-form__submit
类
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 6 – IE safe BEM solution with mixins。
这并不难,那我在抱怨什么呢?好吧,现在假设我们有时希望将此模块放置在侧边栏中。当它在侧边栏中时,我们希望电子邮件输入和按钮彼此堆叠。请记住,为了严格遵守 BEM,我们不允许在样式中使用比单个类更高的特异性。
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 7 – IE safe BEM with mixins in sidebar。
现在 Pug 代码看起来不太容易了,是吗?有几件事导致了这种混乱。
- 容器查询将使这个问题变得容易得多,但它们尚未在任何浏览器中原生存在。
- 围绕 BEM 修饰符语法的問題正在显现。
现在让我们尝试再次执行此操作,但这次使用上下文相关的样式
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 8 – IE safe Context Sensitive with mixins in sidebar。
看看 Pug 标记变得多么简单。在 Pug 标记中,无需担心“如果这样,则那样”的逻辑。所有这些父级逻辑都传递给 CSS,而 CSS 更擅长理解哪些元素是其他元素的父级。
您可能已经注意到,我在最后一个示例中使用了三个类深的选取器。它用于将按钮的宽度设置为 100%。是的,如果您可以证明它,则可以使用三个类选择器。
我不想在每次使用按钮时都应用 100% 的宽度
- 在任何地方使用
- 放置在订阅表单内
- 放置在侧边栏内
我只希望在按钮同时位于订阅表单和侧边栏内时应用 100% 的宽度。处理此问题的最佳方法是使用三个类选择器。
好的,实际上,我更有可能在subscribe-form
元素上使用 ABEM 样式的-verticalStack
修饰符类来应用垂直堆叠样式,或者甚至通过使用EQCSS的元素查询来实现。这意味着我可以将垂直堆叠样式应用于比仅在侧边栏中的更多情况下。不过,为了举例说明,我已将其作为上下文相关的样式。
现在我们了解了上下文相关的样式,让我们回到我之前的那个原始示例,并使用一些上下文相关的样式来应用那个麻烦的grid-column: 3
规则
查看 CodePen 上 Daniel Tonon(@daniel-tonon)的笔 Context sensitive 9 – context sensitive method with mixins。
上下文相关的样式可以简化 HTML 和模板逻辑,同时仍然保留子组件的可重用性。但是,BEM 的每选择器一个类的理念不允许这样做。
由于上下文相关的样式主要与布局有关,因此根据具体情况,您通常应在处理以下 CSS 属性时使用它们
- 应用于子元素的任何与 CSS 网格相关的属性(
grid-column
、grid-row
等)。 - 应用于子元素的任何与 flexbox 相关的属性(
flex-grow
、flex-shrink
、align-self
等)。 - 大于 0 的
margin
值 - 除了
relative
之外的position
值(以及top
、left
、bottom
和right
属性) - 如果
transform
用于定位,例如translateY
您可能还想将这些属性放入上下文相关的样式中,但它们在上下文相关的样式中并不经常需要。
宽度(width)
高度(height)
内边距(padding)
边框(border)
但为了绝对清楚起见,上下文相关的样式**不是**为了嵌套而嵌套。您需要将其视为在 JavaScript 中编写if
语句。
所以对于这样的 CSS 规则
.parent .element {
/* context sensitive styles */
}
您应该将其视为编写以下逻辑
if (.element in .parent) {
.element { /* context sensitive styles */ }
}
还要理解,编写像这样三层嵌套的规则
.grandparent .parent .element {
/* context sensitive styles */
}
应该被视为编写这样的逻辑
if (
(.element in .parent) &&
(.element in .grandparent) &&
(.parent in .grandparent)
) {
.element { /* context sensitive styles */ }
}
因此,如果您确实认为需要这种级别的特异性,请务必编写一个三层嵌套的 CSS 选择器。但是,请了解您编写的 CSS 的底层逻辑。仅使用对您尝试实现的特定样式有意义的特异性级别。
再说一遍,为了超级清楚,**不要为了嵌套而嵌套!**
总结
我完全赞同 BEM 命名约定背后的方法论。它允许将 CSS 分解成小的易于管理的组件,而不是将 CSS 遗留为难以维护的高特异性杂乱无章的状态。
官方 BEM 语法
- 不支持原子设计(Atomic Design)
- 无法轻松扩展
- 需要更长的时间才能让您的思维处理类名的分组
- 在管理大型组件的状态方面非常糟糕
- 试图鼓励您使用单类选择器,而双类选择器更容易维护
- 试图对*所有内容*进行命名空间,即使命名空间带来的问题多于解决的问题。
- 如果正确执行,会使 HTML 变得极其臃肿
我非官方的 ABEM 方法
- 使使用原子设计更容易
- 释放连字符作为可以用于分组的额外方法
- 允许您的思维更快地处理类名的分组
- 擅长处理任何尺寸组件的状态,无论它有多少子组件
- 鼓励*受控的*特异性,而不仅仅是完全*低*特异性,以减少团队混淆并提高网站的可维护性
- 在不需要时避免命名空间
- 保持 HTML 非常简洁,模块上应用的额外类最少,同时仍然保留 BEM 的所有优点
免责声明
我并没有发明-modifier
(修饰符名称前的单个连字符)这个想法。我在 2016 年阅读文章时发现了它。我不记得最初是谁构思了这个想法。如果有人知道这篇文章,我很乐意给他们署名。
更新:2018 年 1 月 21 日(评论回复)
没有人能够链接到我了解-modifier
语法的文章。我所能说的是,我从阅读一篇关于BEVM(block__element–variation -modifier)的文章中了解到的。
以下是一些在我之前提出-modifier
语法的人
如果您喜欢这种方法,BEVM 仍然可以与 ABEM 一起使用(使其成为 ABEVM)。不过,在使用-modifier
语法一段时间后,我最终完全停止了使用&--modifier
语法。当单个连字符在我的 CSS 中更容易使用并且使我的 HTML 更简洁时,我无法真正看到保留双连字符的任何好处。
有些人提到BEMIT与之非常相似。他们是正确的,它确实与 BEMIT 有一些相似之处,但也有一些区别。
您可以将 ABEM 和 BEMIT 在一定程度上合并在一起。有人提到他们更喜欢 BEMIT 中基于状态的类的明确“is”(例如:.is-active
)。这完全没问题,如果您想将“is”添加到 ABEM 中,我建议您像这样编写修饰符.-is-modifierName
。明白驼峰命名法允许自定义分组的含义了吗?
实用程序也可以很容易地从 BEMIT 中迁移过来,它仍然会被写成.u-utilityName
。“实用程序”文件夹/文件可能应该放置在与原子、分子和有机体文件夹相同的目录中。我认为这可能会更容易找到。BEMIT 中的“对象”和“组件”命名空间将不会迁移。它们将被 ABEM 中的原子设计命名空间替换。
评论中一个有趣的讨论是关于在 Sass 中使用@extend
功能。例如,使用<button class='subscribe-form__submit'></button>
和.subscribe-form__submit { @extend .button; grid-column: 3; }
。我认为上下文相关的样式是更好的选择。我非常不同意这种@extend
的实现,除非 CMS 强制你走这条路。您可以在此处查看完整的评论和我的回复:https://css-tricks.cn/abem-useful-adaptation-bem/#comment-1613824。
许多人遇到的一个问题是,我没有深入探讨原子设计是什么以及如何使用它。这超出了本文的范围。如果我试图深入探讨如何使用原子设计原则对组件进行分类,那么这篇文章的长度将很容易翻倍(而且这篇文章本身已经很长了)。我提供了足够的摘要来介绍原子设计的概念,并链接了一个更深入探讨该主题的资源。这大概是我想对解释原子设计给予的关注。
由于原子设计的分类是一个如此令人困惑的话题,我计划撰写一篇完全关于如何对原子设计组件进行分类的文章。我将尝试创建一套清晰的指南供大家遵循,以帮助确定特定组件属于哪个原子设计类别。不过,不要指望它很快就会出现。它发布还需要一段时间。
更新:2019 年 4 月 2 日(原子分类工具)
因此,撰写原子设计文章的计划从未实现。我开始计划一个,但逐渐意识到,写一篇文章试图解释在对模块进行分类时需要考虑的所有细枝末节是错误的做法。一个最终会给你推荐的工具会更有用。
因此,我开始工作,我很高兴地宣布我的全新原子分类工具发布!
这是一个简单的测验,当您回答问题时,它会为您提供每个类别的积分。问题和积分可能不是 100% 完美,但我认为它在大多数情况下都能很好地对组件进行分类。试一试,它比阅读另外几千字的文字要有趣得多。😊
在使用 BEM 一段时间后,我自然地将我的修饰符移动到它们自己的小类中,如文章中所述。不过,让我觉得有点奇怪的是,一个类以
-
开头,而且修饰符并没有太多的上下文。在文章中的示例中,一个元素可以具有-small
、-green
和-active
修饰符。我假设-small
的对应项是-large
,-active
的对应项是-disabled
,另一个颜色修饰符可能是-red
。我还假设您永远不会真正希望某个东西同时是-small
和-large
,因为这没有意义。我发展到这种技术的途径是为修饰符使用数据属性。我相信它仍然具有文章中提出的修饰符的所有优点(简短、可抽象成 js),并且在更具描述性和在 CSS 中看起来不那么笨拙方面也更好。例如,一个修饰符将是
.block__element[data-size="large"][data-color="green"]
。现在我的修饰符按它们修改的事物类型进行分组。您对这种方法有什么想法?
我对此的主要抱怨是涉及的额外输入量。
.-green
比[data-color=green]
少输入很多。这也意味着更简洁的 HTML。我认为将状态与特定属性关联并不重要,因为属性可以隐含。此外,如果应用
.-sticky
类会完全转换标题,那么它属于哪种属性?类在 js 中也比数据属性更容易使用。JS 也能够更快地处理它们。
此外,人们习惯于在 CSS 中使用类而不是数据属性。说服一个大型开发团队以略微不同的方式使用类,比说服他们放弃类而使用数据属性要容易得多,尤其是在这种方法涉及到额外的输入的情况下。这是一个非常难推销的点。
@Daniel 对我来说,更少的输入量并不是一个很好的论据,尤其是在一些额外的字符可以更清楚地展示开发人员的意图的情况下。我理解我们都在不同的团队工作,但对我来说,数据属性应该是任何称职的 Web 开发人员都能处理的,尤其是在团队使用严格的命名约定(如 BEM)的情况下。
对于像 sticky 这样的东西,我只需设置
data-sticky="true"
和data-sticky="false"
。我认为数据属性在 JS 中比类更容易使用。
dataset
现在得到了很好的支持。与其编写一堆逻辑来移除或添加类,不如直接设置数据集。例如,对于 sticky,无需编写添加或移除类的逻辑,只需执行$element.dataset.sticky = isSticky;
,它就会适当地将其设置为 true 或 false。我想
-modifier
最初是由 http://rscss.io 提出的。有趣。这不是我想到的东西,但它似乎呼应了我文章中提出的许多观点,并且也推荐使用
-modifier
技术。我一直使用驼峰命名法(camelCase)来编写 JS 代码。读完这篇文章后,我不知道为什么之前没有在 CSS 中也使用它。它更容易阅读。我被说服了。顺便说一句,这个 ABEM 概念很不错。
实际上,你并没有过多解释“原子设计”的概念。
几年前我用过它,但我认为使用 BEM 会使这个概念过时。块级和命名内部元素在大多数情况下不言而喻。
你仍然可以拥有共享的“原子”或“分子”,并为它们设置自己的块名称。
关于更改修饰符,我完全不同意,因为我认为它带来的问题多于它试图解决的问题(名称冲突,BEM 明确解决的问题)。
如果你想打破 BEM 中的规则,我同意,但要打破“每个选择器一个类”的规则:我完全同意,在一个块/父元素处于特定状态时,在 N 个元素上使用修饰符是没有意义的。但只需在该父元素上应用块/元素修饰符:
accordion__section--open
。然后在子元素上应用上下文样式。这样简单得多。选择器的特异性提高到最低限度,嵌套结构完全易于理解,并且 BEM 保持不变。
顺便说一句,BEM 并不禁止选择器嵌套。“规则”仅仅是任何样式方法中的最佳实践,因为不必要地嵌套会增加特异性,而没有好处。
真正困扰我的一件事是,在“某些情况下”,你的修饰符将是“类似 BEM”的,而在其他情况下则不是。
除了不一致性之外,你开始使用“简短修饰符”进行开发,但当你发现问题时,你只在某些地方将其更改为“类似 BEM”的修饰符。
这种命名选择方式在长期和大型项目中无法维持。规则必须考虑任何情况,否则你永远不知道会发生什么。
从一开始就可以设置并始终遵守上下文样式的嵌套容差,这并不与 BEM 矛盾,但在项目中根据是否发生名称冲突来更改你的命名约定似乎不是一种有效的方法。
这超出了本文的范围。我给出了足够的摘要来介绍这个概念,并且链接到一个更深入探讨该主题的资源。这就是我想对解释原子设计给予的关注。
这是一个合理的观点。如果你想要 100% 的一致性,那么你可以*始终*使用
-componentName--modifierName
的方式来命名修饰符,但你会遇到我在.-small.-green.-active
示例中解释的重复模糊修饰符含义的问题。我发现它解决的问题非常罕见,以至于不值得在所有地方都声明组件名称。我更喜欢使用较短的类名。
感谢你的分享!
一直不明白为什么 BEM 似乎已成为 CSS 的事实标准,类太多了。
我一段时间以来一直在愉快地使用 ._modifier 类,发现它们比 .-modifier 更易读(并且不太模糊?)。
Daniel,好文章!我在最近的一个项目中尝试过非常类似的命名约定,方法是根据给定元素在原子设计模式中的位置对类名前缀。我发现它非常有效,并且正在考虑再次使用它。感谢分享!
但是,这种方法会创建令人讨厌的长类名。我理解很多人可能做不到,但除非你被困在一个无法重构的旧代码库中,否则不要使用这种方法。使用基于组件的目录结构,其中你的 CSS 文件与它的 HTML/JS/其他文件位于同一目录中。
这种方法是一种创建比经典 BEM 让你编写的类名短得多的方法。:/
我*确实*使用基于组件的目录结构,其中我的 CSS 文件与 HTML/JS 文件位于同一目录中。本文并未建议不这样做。本文只是指出了对于已经是 BEM 方法粉丝的人来说,编写 BEM 的更好方法。
你将在 Sass 注释中看到,我在上下文敏感样式示例中说明了 Sass 的不同部分属于哪个组件文件。
不使用 BEM/ABEM 样式命名约定而使用 CSS 进行样式的问题在于,你很快就会遇到特异性问题,其中旧样式变得越来越难以覆盖。
感谢你分享你的想法!
只是一些链接以示公正
https://en.bem.info/methodology/naming-convention/,尤其是 https://en.bem.info/methodology/naming-convention/#alternative-naming-schemes——它可能有助于在官方 BEM 方法和你的方法之间达成共识(因为它们并不相互矛盾)。
很棒的文章!我第一次在本文中遇到
-modifier
模式:https://www.viget.com/articles/bem-sass-modifiers这篇文章不是我想的那篇,但非常相似。也许我会写一个链接列表,列出在我之前提出过这个想法的所有人 lol。
我理解这是一个简化版的 BEM,这是一件好事。但仍然感觉我需要应用火箭科学来编写类名,这使得开发速度变得非常慢。我们公司仍在使用 BEM,但在达到一定规模后,所有东西看起来都变得很奇怪,CSS 和 HTML 源代码都是如此。对于 JS,你也不应该使用 BEM,而应该使用另一个以
js-
为前缀的类。一年多以来,我们更喜欢使用 CSS 模块,这就像从代码库中去除大量重量一样,我可以用像.active
或.button
这样的类,就像在过去一样。当然,我理解这种解决方案并不总是适用于所有项目(旧代码库,无转译等),对于这些项目,BEM 可能是解决方案,但这并不能改变它只是令人头疼的工作这一事实。我认为这是因为没有将 BEM 组件分解成足够小的块。这就是我喜欢原子设计的原因。有时很难确定特定组件属于哪个类别,但尝试对其进行分类的行为有助于训练你的思维以更小的组件为单位进行思考。较小的组件比大型组件更容易重用。
我认为你仍然应该在 js 中使用 BEM。我同意应该在类名中添加
js-
前缀。但我还没有 100% 确定它应该如何添加到 ABEM 中。我目前像这样在原子插槽中添加 js 前缀js-blockName__elementName
,但你失去了与它所属类别的联系。你是如何支持 IE11 的?或者你不需要支持它?
这里有一个有趣的概念。
我不同意使用 .-open 而不是 .block–open 或 .block__element–open。根据我对 BEM 的理解,使用修饰符从来都不是基于状态更改的预期用途。我认为使用基于状态的前缀更具描述性,例如:.is-open、.is-closed、has-loaded 等。使用此方法,你最终会得到 .block.is-open 或 .block__element.is-open,而不是 .block.block–open 或 .blcok__element.block__element–open。
必须说,我对 BEM 的大部分理解都基于 CSSWizardry 对该问题的看法,特别是 BEMIT(https://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further/)。
不过,我认为如果原子设计是您首选的方法,ABEM 确实可能成为命名规范。
很高兴看到有人挑战现状。
我注意到很多人尝试将原子设计应用于 CSS。但作者本人也提到,它实际上与 CSS 无关。
http://atomicdesign.bradfrost.com/chapter-2/#atomic-design-is-for-user-interfaces
在我看来,使用有机体、分子和原子是一种不必要的额外复杂性。每个使用过它的人都知道,很难区分它们之间的差异。你经常会花很长时间争论某物是否是一个分子或一个原子。我更倾向于简单地将所有内容称为
Component
。通过称它们为组件,您也避免了在需要将分子嵌套到分子中时遇到的问题……因为这很奇怪,不是吗?我强烈建议大家使用 Harry Roberts 的CSS 命名空间和BEMIT(包括 ITCSS)。
https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/
https://csswizardry.com/2015/08/bemit-taking-the-bem-naming-convention-a-step-further
你说得对。原子设计仅仅是一个组织组件的概念。在 ABEM 中,atomic 前缀的作用只是告诉您组件位于哪个文件夹中,以便更容易找到组件。除了在开发工具中检查元素时向您提供额外信息之外,它对 css 没有其他影响。如果您不喜欢原子设计,那么我介绍的所有其他概念在删除 atomic 前缀后仍然可以完美地工作。
我必须承认,在对组件类型进行分类时,我也遇到过困难。我通常会纠结于某些东西应该是有机体还是分子。将原子包裹在有机体周围也有些奇怪。我想我会在某个时候写一篇文章来解释如何对原子设计组件进行分类。
尽管我对此有点挣扎,但我仍然很喜欢它。它极大地提高了代码的可重用性。即使你有点搞砸了,尝试将你的组件分类成原子、分子和有机体的过程确实有助于训练你的思维,将事物分解成更小、更可重用的组件。
例如,我过去总是将手风琴编码为一个大的组件。现在我认为手风琴是一个包含一系列小型“显示器”分子组件的大型“手风琴”有机体组件。现在,每当需要显示更多详细信息时,我都可以使用与手风琴有机体分开的显示器分子,而不必每次需要显示器功能时都编写自定义代码。
人们搞砸分类是类名中需要 atomic 前缀的原因。如果一个原子组件确实应该被分类为分子,那么至少在类名上加上 atomic 前缀,人们就不必浪费时间在分子文件夹中搜索,然后才意识到它被保存到原子文件夹中了。如果项目已经到了后期,更改组件所在的文件夹可能风险太大。
这真的取决于实践。一开始你会经常搞砸分类。但是,在使用原子设计一段时间后,您将开始本能地知道哪些组件应该被分类为。
你好,
在我看来,尽管单个修饰符 (
-active
) 简化了所有内容,但在某些情况下它只会搞砸很多事情,我不确定我的团队中的每个人都有勇气开始查找需要添加> * >
来修复它的地方。我认为我们宁愿坚持使用普通的 B.E.M。
另一个很棒的技巧是我在使用 B.E.M 时喜欢的技巧,即使用
@extend
功能。您可以在您的示例中这样使用它
而不是给提交按钮两个类(1.
button
,2.subscribe-form__submit
),您可以只给它subscribe-form__submit
类,然后使用@extend
在其选择器内部添加 .button 样式。.subscribe-form__submit {
@extend .button;
grid-column: 3;
}
HTML 将如下所示
<button class='subscribe-form__submit'>
而不是这个
<button class='button subscribe-form__submit'>
你怎么看?
我对这种方法的主要抱怨是它在你的 css 样式表中创建了大量额外的垃圾。然后,当用户访问你的网站时,你强迫他们下载所有这些额外的垃圾。
假设你的网站上有 50 个按钮扩展了
.button
组件。这是你最终得到的 css。这只是一个带有默认样式和悬停/焦点样式的按钮。您可以看到这变得多么荒谬。
现在将其与以下内容进行比较
按照预期的方式使用类可以使你的 css 保持精简和简洁。
此外,**永远不要**直接扩展类。仅扩展 sass
%
类。直接扩展类可能会导致一些非常奇怪和意外的副作用发生。所以不要这样做
你应该这样做
感谢您分享您的方法。我记得去年也尝试过解决同样的多个几乎相同的类名问题,方法是只使用“element–modifier”并在 CSS 中使用“element, element–modifier { … }”来定义它。你的方法似乎好多了!
没什么大不了的,但像“:first-child”这样的选择器破坏了你的驼峰命名规则。
首先让我说我真的很喜欢这篇文章,并且自从阅读以来一直在思考这个想法。令人沮丧的部分是,我在这篇文章或其他文章中找不到任何关于原子和分子如何相互关联的示例。
例如,如果我有一个标题原子
a-headline
,然后需要将这个原子和一个a-text
原子放入m-section-header
的分子中,但需要文本具有不同的颜色,修饰符在哪里应用?在分子级别还是原子级别?我会将h3
和p
标签包装在一个 div 中并给该 div 添加类似m-section-header__title
和m-section-header__text
的内容,并将颜色更改添加到这些 div 中吗?我已经使用 BEM 大约两年了,并且喜欢集成原子设计的理念,但它让我有点困惑。查看 Kendall 在 CodePen 上的 ABEM 实验 (@KendallWhitman)。
我很高兴你喜欢这篇文章 :)
我并不想在这篇文章中深入探讨原子设计是如何工作的,因为这是一个足够深入的话题,可以单独写一篇完整的文章。这超出了本文的范围。
我想知道你为什么不使用驼峰命名法?我在文章中花了三分之一的篇幅解释了为什么在使用 ABEM 时应该使用驼峰命名法 :/
关于你的问题,它在很大程度上取决于上下文。在什么情况下你希望文本的颜色不同?
如果您只想在特定分子中时更改这些特定原子的颜色,那么最好将颜色更改作为上下文相关的样式。上下文相关的样式不必仅用于布局,这只是它们最常见的用途。每当需要根据父元素应用样式时,都应使用它们。
如果您希望能够以更通用的方式应用此颜色更改,那么最好直接将
-colourName
类应用于原子。如果您希望两者关联,您可以将
.-colourName
类应用于分子,然后在原子 scss 文件中执行以下操作&.-colourName, .- colourName & { ... }
。如果修饰符应用于元素本身或父元素,这将应用颜色。我认为在您的示例中处理它的最干净方法是将修饰符应用于分子并使用
&.-colourName { color: [colour-name]; }
来应用颜色更改,因为颜色可以继承。不过,这一切都取决于上下文以及在什么情况下您希望应用颜色。
我认为在您的示例中处理它的最干净方法是将修饰符应用于分子并使用
&.-colourName { color: [colour-name]; }
来应用颜色更改,因为颜色可以继承。感谢您分享您的方法。我还建议查看 SUIT CSS 命名规范
https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md
在我看来,SUIT 组件状态类通过添加
is-
前缀而不是单个连字符更明确。没有理由不能将
is-
合并到 ABEM 修饰符语法中。您可以执行以下操作.-is-modifierName
。明白我的意思了吗?驼峰命名法允许您创建自己的自定义分组?我喜欢在开头保留连字符,因为它仍然是一个修饰符名称,并且连字符开头 = 修饰符。
Harry Roberts 几年前提出了类似的事情。从那以后我一直在使用某种形式的它。
https://csswizardry.com/2015/03/more-transparent-ui-code-with-namespaces/
可能你的
-modifier
概念来自阅读关于 BEVM 的文章。BEVMblock__element--variation -modifier
是一个 BEM 变体,它添加了可链接的修饰符。我已经使用某种形式的它几年了。是的,它来自一篇关于 BEVM 的文章 :)
我尝试使用 -V 语法一段时间,但最终放弃了它,转而始终使用 -M 语法。我找不到任何 -V 比 -M 语法更有用的情况,并且我阅读的文章建议 -V 并不重要,如果需要,您可以忽略它。
关于
-modifier
你可能偶然发现了我在 medium 上发布的 2016 年 3 月的文章,并在与 Yandex 的开发人员交谈后删除了它。
我们出于多种原因从项目中删除了
-modifier
1. IE 和一些移动浏览器不支持以
-
开头的类。2. 特定性的问题在于,有人开始使用链式修饰符,然后我们就回到了特定性地狱。
(例如
btn -icon -tiny
会覆盖-tiny
的填充规则,使用类似.btn.icon.tiny
的方式为较小的图标按钮设置固定的高度和宽度,其他修饰符由于特定性问题而停止工作)顺便说一下
BEM 是一种方法论,命名约定只是其中的一部分。
https://en.bem.info/
关于第1点,我知道 IE11 支持在类名开头使用连字符前缀。其他所有 IE 浏览器现在基本上已经淘汰了。
我感兴趣的是看看哪些移动浏览器不支持它。我猜测 Opera Mini 不支持,因为它几乎不支持任何东西。幸运的是,这并不是我个人需要支持的浏览器。
至于第2点,我仍然认为分离修饰符更好。你提出的问题听起来更像是缺乏培训或资源的问题(因此 BE 开发人员必须编写 FE 代码)。如果使用得当,它可以使网站更容易维护,并且 HTML 更易于阅读。
你好,
感谢这篇文章!我只触及了 BEM 的表面,我一直对 OOCSS 中单一类选择器上的这条规则有不好的感觉,并且在 BEM 中应用了外观可怕的类。
现在,由于通过组合类添加一些特定性似乎完全合乎逻辑,那么是否可以,而不是使用类,在有意义且可能的情况下使用 aria 属性,就像这里提出的那样:http://alistapart.com/article/meaningful-css-style-like-you-mean-it#section3
特定性将与类相同,并且我们将受益于更语义化的方法。
你怎么看?
我不反对这个想法。
我不同意文章背后的全部前提,即他希望我们尽量避免使用类。我担心特定性会失控。此外,[type=text] 并没有涵盖所有其他文本框样式的输入,例如电子邮件。我还认为这会使样式表变得有点凌乱。
不过,在可能的情况下,使用语义化的 aria 属性进行基于状态的样式设置而不是修饰符类,这并不是一个坏主意。
使用 BEM 仍然不够好,为什么功能性 CSS 可以在这些情况下起作用,而你可以使用模板引擎或 React/Vue/Hyperapp 来根据数据重用你的组件和修饰符呢?
在大多数情况下,功能性 CSS 就像语义化 HTML 一样更容易查看。
我不喜欢使用 Basscss 和 Tachyons 等原子 CSS 库。
这篇文章很好地概括了这种类命名风格的所有问题
“原子 CSS 的问题” https://medium.com/simple-human/the-problem-with-atomic-css-d0c09c7aa38e
听起来你像是 CSS-in-JS 的粉丝。
首先,CSS-in-JS 只有在网站完全使用 React/Vue 等构建时才有效。以这种方式构建网站只有在状态不断变化的情况下才有意义。对于像简单的宣传册风格的网站,几乎没有状态变化需要担心,不应该用 React/Vue 构建。
其次,我个人不喜欢 CSS-in-JS。这主要是一个个人偏好的问题,但这篇文章很好地概述了为什么你可能要避免使用 CSS-in-JS
“停止在 Web 开发中使用 JavaScript 中的 CSS” https://medium.com/@gajus/stop-using-css-in-javascript-for-web-development-fa32fb873dcc