CSS 类名命名规范:kebab-case、BEM 及其他方案

大多数 CSS 类名都写成小写的 kebab-case(短横线命名),比如 .site-header、.price-tag,因为 CSS 给自己的东西命名时就是这么写的:background-color、font-size、flex-direction。BEM 在此基础上加了一套固定结构 block__element--modifier,所以像 card__title--large 这样的类名能告诉你三件事:它属于哪个组件、是组件的哪个部分、显示的是哪种变体。
下文会讲:CSS 中哪些地方区分大小写、哪些不区分,kebab-case 为什么成了默认选择,BEM 的命名规则究竟该怎么理解,以及什么时候换一种方案就用不着 BEM 了。
CSS 中哪些区分大小写,哪些不区分
CSS 本身的语法在 ASCII 范围内不区分大小写,所以 COLOR: RED 和 color: red 的效果完全一样。但有三样东西不同:
- 类名区分大小写。
.Card匹配不到class="card"。唯一的例外是处于怪异模式(quirks mode)的页面,浏览器会不区分大小写地匹配类名,这也是每个页面都应该以<!DOCTYPE html>开头的又一个理由。 - 自定义属性区分大小写。
--brand-color和--Brand-Color是两个不同的变量。 - HTML 元素名和属性名不区分。在 HTML 文档中,
DIV和div选中的是相同的元素;在 XML 中,二者就不同了。
想彻底不再操心这件事,最简单的办法就是让所有类名和自定义属性都保持小写。至于其他语言和文件系统如何处理大小写,请参阅我们的大小写敏感性对比。
为什么 CSS 类名最终选择了 kebab-case
CSS 并没有强制类名使用 kebab-case,.cardTitle 和 .card_title 都是合法的。用连字符之所以顺理成章,是因为类名周围的一切本来就在用它:属性名、space-between 这样的值,以及 aria-label 和 data-user-id 这样的 HTML 属性。在满是连字符的样式表里冒出一个 camelCase(驼峰命名)类名,看起来就像打错了字。
kebab-case 在 JavaScript 里会有些别扭,因为标识符中不允许出现连字符。浏览器会处理好它自己的那部分:data-user-id 读作 element.dataset.userId,background-color 变成 element.style.backgroundColor。类名通常仍以字符串形式出现(classList.add("is-open")),所以不受影响。
例外是 CSS Modules。CSS Module 会把类名作为一个对象导出给 JavaScript,所以 .card-title 必须写成 styles["card-title"],而 .cardTitle 则可以写成更整洁的 styles.cardTitle。这是唯一一种我们会把类名改成 camelCase 的场景。
BEM 命名:块、元素、修饰符
BEM(Block、Element、Modifier,即块、元素、修饰符)出自 Yandex。这套方法论的历史页面把相关工作追溯到 2005 年,把 Yandex 于 2009 年 3 月发布 Lego 2.0 框架视为 BEM 的开端,并把开源的时间定在 2010 年。它的命名规则很简短:
- 块(Block):一个独立的组件,单独拿出来也有意义,比如
.card、.search-form或.menu。 - 元素(Element):块的一部分,离开块就没有意义,命名方式是块名、两个下划线、元素名:
.card__title、.menu__item。 - 修饰符(Modifier):改变外观、行为或状态的标记,命名方式是块名或元素名、两个连字符、修饰符名:
.card--featured、.card__button--disabled。修饰符也可以是一个键加一个值:.form--theme-xmas。
每个部分内部的单词用单个连字符分隔,例如 .search-form__submit-button。下面是一个完整的组件:
<article class="card card--featured">
<h2 class="card__title">Spring sale</h2>
<p class="card__body">Everything 20% off this week.</p>
<a class="card__button card__button--disabled" href="/sale">Shop now</a>
</article>.card { border: 1px solid #ddd; padding: 1rem; }
.card--featured { border-color: gold; }
.card__title { font-size: 1.25rem; }
.card__button--disabled { opacity: 0.5; pointer-events: none; }经典 BEM 与双连字符风格
如果你去看这套方法论的官方网站 bem.info,会发现修饰符的写法不一样。它的经典方案用单个下划线分隔修饰符:menu_hidden、menu__item_visible、menu__item_type_radio。-- 写法在那里被列为一种替代方案,即“Two Dashes style”(双连字符风格),getbem.com 介绍的也是这种写法。两种都是正统的 BEM,只是不要在同一个代码库里混用。bem.info 还提到双连字符有个小问题:HTML 注释里出现双连字符,可能无法通过验证。
容易用错的 BEM 规则
- 不要出现元素的元素。卡片头部里的标题仍然是
card__title,而不是card__header__title。在 HTML 中,元素想嵌套多深都可以,名称却始终保持扁平。bem.info 给出的理由是:如果名称层层串联,你就无法在不改动现有代码的情况下移动、添加或删除元素。 - 修饰符从不单独使用。要写
class="button button--primary",而不是class="button--primary"。修饰符只是对基础类的补充。 - 只用单个类选择器。写
.card__title,而不是.card .card__title或h2.card__title。公认的例外是会改变其内部元素的块修饰符:.card--featured .card__title。 - 按用途给修饰符命名。这一条是我们的看法,并非 BEM 规则:
--featured或--error经得起改版,而--yellow在品牌色更换的那天就名不副实了。 - 别把 JavaScript 钩子混进样式类。这也是我们的看法:在脚本中用
data-action="close"这样的 data 属性来选择元素,而不是用.card__button,这样出于样式原因重命名类时,就不会破坏功能。
以后要重命名某个块,基本上就是查找和替换。把模板粘贴到我们的查找与替换工具中,勾选“正则”,把 card(__|--) 替换为 product-card$1:所有元素类和修饰符类都会换成新的块名,匹配数会告诉你改了多少处。不带后缀的 card 类要单独重命名,以免误改无关的单词。
用 Sass 和原生 CSS 嵌套编写 BEM
Sass 的父选择器可以给外层选择器加后缀,它的文档用的例子正是 BEM。原生 CSS 嵌套做不到这一点,MDN 也明确指出了这一点:嵌套的 &__title 会被当作类型选择器,所以不会生成 .card__title。
// Sass: works
.card {
&__title { font-size: 1.25rem; } // compiles to .card__title
&--featured { border-color: gold; } // compiles to .card--featured
}/* Native CSS nesting: write the full class names */
.card {
border: 1px solid #ddd;
&.card--featured { border-color: gold; } /* .card.card--featured */
}
.card__title { font-size: 1.25rem; }BEM 之外:什么时候不需要它
BEM 的存在,是为了防止类名冲突,并在全局样式表中让结构一目了然。如果别的方式已经解决了冲突问题,BEM 那些长名称带来的好处就少了:
- CSS Modules 默认把类名限定在局部作用域内,所以一个组件里的普通
.title不会和另一个组件里的.title冲突。 - Utility-first CSS(工具类优先的 CSS),比如 Tailwind,基本上用
font-bold这类预定义的工具类取代了自定义类名,于是命名工作转移到了组件里。 - 小型网站只有少数几个组件,通常用普通的 kebab-case 再给每个组件加个前缀就够了。
即便如此,凡是你确实要自己写的类名,kebab-case 仍然是稳妥的默认选择。
经验法则
- 类名和自定义属性一律使用小写 kebab-case。
- 多人共用一份全局样式表时用 BEM:
block__element--modifier,元素名保持扁平,修饰符与基础类并列。 - 只有当 CSS Modules 这类工具把类名暴露给 JavaScript 时,才使用 camelCase 类名。
要把组件名变成类名?在我们的大小写转换器中输入“Product Card”,点击 kebab-case 按钮就能得到 product-card。至于项目中其他各层的命名规范,从环境变量到 URL,请参阅我们的开发者命名风格速查表。


