说实话,刚写CSS那几年,我最头疼的不是Flex布局调不对,也不是浏览器兼容,而是打开一个项目看到一摞这样的class:
<div class="content_box contentBox2 f-l clearfix none p-10">每个英文单词拆开都能看懂,合在一起完全不知道这个元素的职责是什么。更崩溃的是,想改一个按钮样式,搜“btn”能搜出几百条结果,根本分不清哪个是哪个。后面接触了BEM命名方法,才发现自己一直缺的不是CSS技巧,而是一套给类名“定规矩”的方法论。这篇文章我就把自己从BEM入门到实际落地踩过的坑、总结出的经验,完整梳理一遍。
BEM是Block(块)、Element(元素)、Modifier(修饰符)三个英文单词的缩写。简单说,它是一套让CSS类名自带“结构说明书”的命名规范,核心语法长这样:
.block__element--modifier这个方法论最早由俄罗斯的Yandex团队提出,用于解决大型项目样式难以维护的问题。现在你能看到的大多数主流组件库,底层类名都脱胎于这套思路。适合刚入门前端想建立工程化习惯的同学看,也适合已经在业务项目里被命名问题折磨、想给团队定规范的人参考。
1. BEM命名方法的核心理念与语法规则——为什么类名需要“说明书”
1.1 一句话理解BEM:给类名画一张结构图
BEM的核心思想是把页面拆成三层角色:块是独立、可复用的组件单元,元素是块的组成部分,修饰符控制状态和外观变化。三者组合成一个类名,就能在一行代码里看到完整的信息。
我常用一个生活化的类比来理解:写快递地址。块是“城市”,元素是“街道和门牌号”,修饰符是“备注:放快递柜”。如果不写清楚,快递小哥找不到门;类名如果不写清楚,三个月后的你自己也找不到那段样式在哪。
用一个具体例子来说:
<button class="button button--primary">提交</button>button是块,button--primary是这个块的一个修饰符变体。看到这个类名,你就知道:这是个按钮、是主色调样式,不需要再额外翻CSS注释。
再看一个带元素的结构:
<div class="card"> <h3 class="card__title">标题</h3> <p class="card__desc">描述内容</p> </div>card__title里的双下划线表示:title 这个元素“属于” card 这个块,没有独立存在意义。整个卡片的结构在HTML层面就已经可视化地表达出来了。
1.2 为什么偏偏用双下划线和双中划线
第一次看到.block__element--modifier这种写法,很多人第一反应是“太长了”或者“好丑”。但Yandex团队选这两个符号是有讲究的。
单下划线_在类名里一般是“单词连接符”,比如content_box,它表示“这是两个词组成的一个名字”。如果BEM再继续用单下划线去区分块和元素,就会非常混淆:
/* 到底是“内容盒子”还是“内容这个块的盒子”? */ .content_box双下划线__是一个足够醒目的分隔标记。同理,单词之间的连接符用中划线-,比如my-button,而修饰符用双中划线--和块元素区分开。这样在看到类名的一瞬间,就能准确判断哪部分是块名、哪部分是元素名、哪部分是修饰符,三条信息不会互相污染。
从工程实践角度看,这种符号选择还有一个现实好处:搜索非常方便。在代码库里搜button--primary,精确命中目标;搜card__title,不会再混进别的card_title。我用Chrome DevTools调试的时候深有体会,BEM命名的元素,Elements面板里一扫就能定位结构,省去大量翻DOM的时间。
1.3 命名即文档:调试和维护里的隐形收益
类名写清楚了,能省下什么?省下“文档同步”这件事。
传统命名方式下,一个组件改版,HTML结构调整了,但CSS里还是老类名,两边的对应关系很容易断掉。BEM把结构关系织进了类名本身,HTML和CSS之间的对应是“强绑定”的。比如看到HTML里card__footer这个类,CSS那边就必须有这个类名的独立样式块;看到card--featured,就知道这是一个变体样式。哪边漏了,一眼就能发现。
这点在接手别人的项目时尤其明显。我一个同事曾经接手过一个外包项目,原开发用.box1 .box2 .contentright这种命名,他花了两周才理清样式脉络。后来我们用BEM重构了一版,结构清晰到产品经理都能对着HTML猜出页面布局。这套命名方法,本质上是在为团队降低沟通成本。
2. Block、Element、Modifier的划分边界与设计要诀
2.1 Block块:独立可复用的“零件”
块是页面里能独立存在的最小复用单元。判断标准很简单:这个组件脱离当前页面环境,放到另一个页面里还能正常工作吗?如果能,它就应该是一个块。
实际项目里常见块有很多:按钮、导航栏、轮播图、分页器、搜索框、表单、弹窗内容区。块的名字要体现它的职责,而不是它的外观。red-button不如primary-button,因为前者描述的是“当前长什么样”,后者描述的是“它承担什么角色”。外观会变,角色一般不会。
命名块的时候有一个常见误区:把位置信息写进块名。left-sidebar-card就是反面教材——今天它可能确实在左边栏,明天UI改版挪到右边,类名就要跟着改一遍,自找麻烦。正确的做法是叫profile-card,它是一张“用户资料卡”,放在哪由布局层决定,不由组件自己决定。
2.2 Element元素:属于块的“内部件”
元素是块的组成部分,脱离块它没有任何意义。卡片里的标题、图片、正文、按钮,导航里的logo、菜单项、链接,这些都是典型的元素。
BEM对元素的写法是扁平化的:块名加双下划线加元素名。这里有一个非常重要的实操细节:不管元素在DOM里嵌套多深,类名里都不需要体现“层中层”的关系。
举个反例和正例。页面结构是这样:
<div class="card"> <div class="card__header"> <h3 class="card__header__title">标题</h3> </div> </div>card__header__title看似把层级写清楚了,实际却是个陷阱。一旦视觉改版,标题不再放在header里,类名就全乱了。正确的做法是:
<h3 class="card__title">标题</h3>HTML里的层级关系由DOM结构表达,类名只表达“title 属于 card”这一层归属关系。职责归职责,嵌套归嵌套,两件事别混在一起。这个扁平化原则,是BEM在实际项目中减少返工的关键。
2.3 Modifier修饰符:状态和变体的“开关”
修饰符用来表达两类信息:一类是外观或行为变体,比如大按钮、主按钮、禁用态;另一类是特定状态,比如 active、disabled、hover。
语法上,修饰符可以挂在块上,也可以挂在元素上:
/* 块的修饰符 */ .button--primary /* 元素的修饰符 */ .card__title--highlight实操中,修饰符最常见的写法是“叠加类”,而不是“替换类”。什么意思?看下面的对比。
<!-- 错误:替换基础类,会丢失块的基本样式 --> <button class="button--primary">提交</button> <!-- 正确:基础类加修饰符类,叠加生效 --> <button class="button button--primary">提交</button>对应的CSS是这样:
.button { padding: 8px 16px; border: none; border-radius: 4px; } .button--primary { background: #1677ff; color: #fff; }基础类负责公共结构,修饰符类只负责差异项。这样写的好处是,新增一个变体只需要加一个.button--danger、.button--success,完全不用动基础类。这是BEM和面向对象思路很接近的地方:公共部分抽出来,差异部分靠组合。
还有一个判断技巧:修饰符是布尔型还是键值型。布尔型直接写--disabled,表示“有没有这个状态”;键值型写成--theme: dark或.card--theme-dark,表示“这个状态的值有多种选项”。前者适合开关类状态,后者适合多主题这类枚举场景。
2.4 粒度判断经验:怎么决定一个元素是块还是元素
这是BEM最容易纠结的地方,我要重点说说我的判断维度。
我自己的经验是看复用范围。一个部分如果只服务于当前组件内部,那就是元素;如果它可能被多个不相关的场景复用,那就应该提成独立的块。
举个实际例子。一个文章卡片里有“点赞按钮”,我开始时把它写成了card__like-btn,后来发现用户动态列表、评论区都要用这个点赞按钮,只是容器不同。那这个按钮就应该独立成块.like-btn,卡片、列表、评论三个场景各自引用它。
另一个例子是“壳”。一个模态框组件,它可能有标题、内容、脚部,这些都是modal__title、modal__content、modal__footer元素。但如果模态框的内容本身是一张复杂的用户表单,这个表单就应该独立成块.user-form,而不是modal__form。因为表单可能被用在非模态框的场景里。
我有一个简单口诀:组件内部的小部件,先按元素写;发现第二个地方要用,立刻提成块。不用一开始就追求完美,重构的成本并不高。
3. 完整实操:用BEM重写一个前端页面组件
3.1 案例目标和分析步骤
纸上谈兵没意思,我拿一个真实业务组件走一遍完整流程。假设我们要做一个“文章卡片”,包含封面图、标题、摘要、标签列表、底部操作区(阅读量和点赞按钮),还要支持一个“精选”高亮变体。
拿到设计稿以后,我的分析步骤一般是固定的三步。
第一步,找块。文章卡片作为一个整体可以被列表页、推荐位、搜索结果页复用,所以它本身是一个块,命名为.article-card。
第二步,找元素。卡片内部有封面article-card__cover、标题article-card__title、摘要article-card__summary、标签容器article-card__tags、底部操作区article-card__footer。
第三步,找修饰符。精选卡片有特殊高亮边框,用article-card--featured。标签有不同类型,比如技术、生活,用article-card__tag--tech这类元素修饰符来区分颜色。
这里有个容易犯的错误:直接把底部操作区里的点赞按钮叫article-card__like-btn。还是我前面说的那个原则,点赞按钮如果只在卡片里出现,可以这么写;如果其他地方也要用,独立成.like-btn块。我在这个例子里假设它只在卡片内用,就保留为元素。
3.2 HTML结构该怎么写
清晰类名对应的HTML长这样:
<article class="article-card article-card--featured"> <img class="article-card__cover" src="cover.jpg" alt="cover" /> <div class="article-card__body"> <h2 class="article-card__title">BEM命名方法完全指南</h2> <p class="article-card__summary">一套让CSS类名结构化的命名方法论</p> <div class="article-card__tags"> <span class="article-card__tag article-card__tag--tech">前端</span> <span class="article-card__tag article-card__tag--life">生活</span> </div> <div class="article-card__footer"> <span class="article-card__meta">阅读 2.1k</span> <button class="article-card__like-btn">点赞</button> </div> </div> </article>对比一下传统写法的HTML会是什么样的:
<article class="card featured"> <img class="cover" src="cover.jpg" alt="cover" /> <div class="body"> <h2 class="title">...</h2> <p class="summary">...</p> <div class="tags"> <span class="tag blue">前端</span> <span class="tag green">生活</span> </div> </div> </article>第二版看起来短,问题在于:.cover在全局里极小概率重名吗?不,大概率重名。.tag.blue的语义完全靠人脑记忆,CSS和HTML的对应关系也是松散的。用BEM版本,即使不打开CSS文件,每个类名都在自述它是什么、属于谁、处于什么状态。
3.3 CSS该怎么写:结构样式和变体分离
对应HTML,CSS的写法遵循一个原则:选择器简单化,样式职责单一化。我给出完整示例:
/* 块的基础样式 */ .article-card { border: 1px solid #e5e6eb; border-radius: 8px; overflow: hidden; background: #fff; transition: box-shadow 0.2s; } /* 元素的独立样式 */ .article-card__cover { width: 100%; height: 200px; object-fit: cover; } .article-card__title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .article-card__summary { font-size: 14px; color: #666; line-height: 1.6; } .article-card__tags { margin: 12px 0; } .article-card__tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; background: #f2f3f5; } /* 修饰符:差异化样式 */ .article-card--featured { border-color: #1677ff; box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15); } .article-card__tag--tech { background: #e6f4ff; color: #1677ff; } .article-card__tag--life { background: #f6ffed; color: #52c41a; }注意几个细节。修饰符类选择器都放在结构样式后面,优先级相同的情况下,后者覆盖前者;基础类结构不能随便删属性,因为修饰符只依赖覆盖。还有就是CSS的引入方式上,我建议一个组件跑一个独立文件,用外链方式按需加载,不要在全局样式表里堆上千行组件样式。这样组件不用的页面不会加载多余CSS,维护边界也清楚。
组件级CSS文件里的类名,天然被限定在组件内部引用,不会和全局公共样式互相污染。这就是BEM和CSS工程化结合的基本玩法。
3.4 嵌套和层级:BEM是不是禁止写后代选择器
很多人误以为BEM不能写后代选择器,必须保证所有选择器都是单类名。这是天大的误解。BEM追求的是类名语义不依赖嵌套,但CSS选择器里偶尔的后代选择器完全可以使用。
什么情况适合用后代选择器?在一个块的作用域内,临时调整所有子元素的样式,用:
.article-card:hover .article-card__title { color: #1677ff; }这比给title单独再写一个--hover修饰符清晰得多。但要注意控制嵌套深度,建议最多不超过两层。一旦CSS选择器嵌套超过四层,可读性和性能都会下降,通常意味着设计本身就过于复杂了。
从HTML结构上讲,BEM真正推荐的是“类名扁平化”,而不是“DOM扁平化”。DOM该嵌套就嵌套,类名始终保持 block 和 block__element 两层语义就够。
4. BEM与前端生态的配合:预处理器、CSS Modules与组件库
4.1 BEM加SCSS:用嵌套语法减少重复前缀
原生CSS写BEM有个痛点,article-card__title、article-card__footer里面的article-card反复写,代码看着累。引入SCSS之后,可以用&符号把重复前缀抹掉:
.article-card { border: 1px solid #e5e6eb; &__title { font-size: 18px; } &--featured { border-color: #1677ff; } &:hover &__title { color: #1677ff; } }编译出来的结果和手写长类名一模一样,但源码的维护体验好很多。这里有一个我踩过的坑:不要为了图省事,把修饰符和元素写成“嵌套加深”的方式,比如:
.article-card { &__footer { &__like-btn { // 这个会被编译成 .article-card__footer__like-btn } } }前面说过,元素扁平化是原则,编译出的长串命名会让结构信息失真。SCSS只是帮你少写字,不是帮你重新发明命名结构。
对于顶级块的边界,还有一招@at-root可以把某些样式“踢”出作用域,不过一般业务项目用不上,知道有这回事就行。
4.2 在React和Vue组件化项目中怎么用BEM
现代前端开发几乎都是组件化开发了,BEM和组件化天生合拍。一个组件对应一个块,组件内部的所有元素都挂在块名下,组件的props或state转换成修饰符类名。
在React里,我通常结合classnames这个库来写:
import classNames from 'classnames'; function Button({ type = 'default', size = 'md', disabled = false }) { const className = classNames( 'button', `button--${type}`, `button--${size}`, disabled && 'button--disabled' ); return <button className={className} disabled={disabled}>{children}</button>; }Vue的话可以直接用数组和对象语法,逻辑是一样的。
这里我建议优先用BEM原生类名加CSS Modules的局部作用域组合。为什么不用纯CSS Modules?因为CSS Modules默认生成的混淆类名,会丢失BEM带来的“类名即结构文档”的价值,调试的时候看不到语义。我的习惯是:保留BEM语义类名,再用:local限制作用域,鱼和熊掌兼得。
4.3 和原子化CSS、Tailwind的关系:不是替代关系
这几年原子化CSS热度很高,尤其是Tailwind,把很多样式拆成flex、p-4、text-center这样的小工具类。热词里提到的“原子性css”就是这个方向。我经常被问BEM和Tailwind哪个好,我的看法是:它们根本不是同一层的东西。
BEM解决的是“类名的语义边界”——这个类代表什么组件、什么元素、什么状态。Tailwind解决的是“样式属性的快速组合”——间距、布局、字号这类一次性的视觉调整。
实际项目里,我的组合策略是:布局和间距这类通用样式用工具类,比如排列卡片的外层容器;组件内部的结构样式和状态变体用BEM类。比如:
<div class="flex gap-4"> <article class="article-card article-card--featured"> <h2 class="article-card__title">...</h2> </article> </div>这样既不会让BEM类膨胀到几十个,也不会因为纯工具类导致完全不知道DOM表达什么业务含义。BEM在大型业务系统、设计系统组件库里依然值得用,因为那些场景需要稳定的可维护的命名契约。
5. 常见问题与避坑指南——实战中的经验总结
5.1 类名太长、太啰嗦怎么办
BEM最被吐槽的就是命名长,缩进一多,HTML里全是长类名。我的处理方式有四种。
一种是用SCSS嵌套减少源码里的重复,刚才说过。第二种是块名的单词精简,article-card不写成article-information-card-container,能一眼看懂就是合适的长度。第三种是修饰符上涨时定义简写,但团队要约定好,比如--sm、--lg、--primary作为标准枚举。第四种是,关键边界在于:相同语义的修饰符尽量复用,不要一个组件一套新词。
有一点要说清楚,类名长不太影响性能。类名长度和CSS选择器匹配性能的关系,在现代浏览器里基本可以忽略不计,业务项目真正的瓶颈不在这。别为了省几个字符牺牲可读性。
5.2 BEM和工具类、公共类并存会冲突吗
并存本身没冲突,就怕没有边界。我见过一个项目,全局工具类.mt-20、.clearfix满天飞,组件类名和工具类交织在一起。
我的解决思路是这样:工具类只用于布局和不可复用的临时调整,组件内的元素样式全部由BEM类负责。禁止在组件内部给同一个元素同时挂BEM类和一个可能影响相同属性的工具类,比如class="card__title text-lg"这种组合,一旦样式顺序不稳定,就是给排查问题挖坑。
真出现冲突时,优先级规则要统一:工具类放前,组件类放后,或者反过来,但整个项目必须一个标准。最省心的做法是写一条校验规则到ESLint里,限制工具类和BEM类不能同时出现在同一元素的class列表里。
5.3 团队规范怎么落地,而不是写在文档里吃灰
BEM规范光有一篇Markdown文档是不够的,落地要配合工具和流程。
我团队里的实际组合是:约定规范文档加Stylelint插件校验类名格式,再加Code Review把关边界划分。Stylelint可以配置selector-class-pattern,强制类名匹配BEM的正则,不通过直接报错,从工具层面卡住格式。Code Review阶段主要看语义划分:某个部分是块还是元素、修饰符写得合理不合理。
还有个小技巧:新成员入职的前两周,要求他们写组件前先画一个“类名清单”(就是先列出要用的所有类名),评审通过再写代码。这个习惯能让人强迫自己思考组件结构,而不是边写边乱起名。
5.4 从面试角度看BEM:常见面试题怎么答
既然很多前端岗位面试会问命名规范,我顺手把这个问题梳理一遍,也算给正在准备面试的朋友一个参考。
面试官问BEM,往往不只看你会不会写,还看你对“为什么需要规范”有没有体感。回答框架可以是:
- 为什么需要:大型项目多人协作,类名冲突和维护成本会指数级上升,需要一套语义化、结构化的命名约定。
- BEM解决什么:把页面拆成块、元素、修饰符三层,让类名自带归属和状态信息,可读性高、复用性好、调试成本低。
- 具体语法和例子:现场写出按钮的
.button.button--primary结构。 - 短板:类名偏长、粒度划分有主观性,需要团队共同维护约定。
- 演进:结合SCSS减少重复,结合CSS Modules做作用域隔离,或者对比CSS-in-JS方案看适用场景。
回答的时候如果能带上真实项目里“命名混乱到重构”的案例,会比背诵定义打动人得多。面试官不是要你背标准答案,是看你对工程问题的真实思考。
最后再分享一点个人习惯。我在写任何组件之前,会先花一两分钟把设计稿里的结构在纸上拆成“块、元素、修饰符”三列清单,写清楚再开代码。这个习惯帮我在项目后期省下了大量改类名的时间。BEM不是银弹,但它是我用过的CSS命名方案里,最适合团队协作、最经得起时间考验的一套。如果你还没试过,挑个实际组件项目练一次手,感受过前后对比,你就知道这件事的长期价值在哪里了。