BEM. Like seemingly all techniques in the world of front-end development, writing CSS in a BEM format can be polarizing. But it is – at least in my Twitter bubble – one of the better-liked CSS methodologies.
Personally, I think BEM is good, and I think you should use it. But I also get why you might not.
Regardless of your opinion on BEM, it offers several benefits, the biggest being that it helps avoid specificity clashes in the CSS Cascade. That’s because, if used properly, any selectors written in a BEM format should have the same specificity score (0,1,0
). I’ve architected the CSS for plenty of large-scale websites over the years (think government, universities, and banks), and it’s on these larger projects where I’ve found that BEM really shines. Writing CSS is much more fun when you have confidence that the styles you’re writing or editing aren’t affecting some other part of the site.
There are actually exceptions where it is deemed totally acceptable to add specificity. For instance: the :hover
and :focus
pseudo classes. Those have a specificity score of 0,2,0
. Another is pseudo elements — like ::before
and ::after
— which have a specificity score of 0,1,1
. For the rest of this article though, let’s assume we don’t want any other specificity creep. 🤓
But I’m not really here to sell you on BEM. Instead, I want to talk about how we can use it alongside modern CSS selectors — think :is()
, :has()
, :where()
, etc. — to gain even more control of the Cascade.
What’s this about modern CSS selectors?
The CSS Selectors Level 4 spec gives us some powerful new(ish) ways to select elements. Some of my favorites include :is()
, :where()
, and :not()
, each of which is supported by all modern browsers and is safe to use on almost any project nowadays.
:is()
and :where()
are basically the same thing except for how they impact specificity. Specifically, :where()
always has a specificity score of 0,0,0
. Yep, even :where(button#widget.some-class)
has no specificity. Meanwhile, the specificity of :is()
is the element in its argument list with the highest specificity. So, already we have a Cascade-wrangling distinction between two modern selectors that we can work with.
The incredibly powerful :has()
relational pseudo-class is also rapidly gaining browser support (and is the biggest new feature of CSS since Grid, in my humble opinion). However, at time of writing, browser support for :has()
isn’t quite good enough for use in production just yet.
Lemme stick one of those pseudo-classes in my BEM and…
/* ❌ specificity score: 0,2,0 */
.something:not(.something--special) {
/* styles for all somethings, except for the special somethings */
}
Whoops! See that specificity score? Remember, with BEM we ideally want our selectors to all have a specificity score of 0,1,0
. Why is 0,2,0
bad? Consider a similar example, expanded:
.something:not(a) {
color: red;
}
.something--special {
color: blue;
}
Even though the second selector is last in the source order, the first selector’s higher specificity (0,1,1
) wins, and the color of .something--special
elements will be set to red
. That is, assuming your BEM is written properly and the selected element has both the .something
base class and .something--special
modifier class applied to it in the HTML.
Used carelessly, these pseudo-classes can impact the Cascade in unexpected ways. And it’s these sorts of inconsistencies that can create headaches down the line, especially on larger and more complex codebases.
Dang. So now what?
Remember what I was saying about :where()
and the fact that its specificity is zero? We can use that to our advantage:
/* ✅ specificity score: 0,1,0 */
.something:where(:not(.something--special)) {
/* etc. */
}
The first part of this selector (.something
) gets its usual specificity score of 0,1,0
. But :where()
— and everything inside it — has a specificity of 0
, which does not increase the specificity of the selector any further.
:where()
allows us to nest
Folks who don’t care as much as me about specificity (and that’s probably a lot of people, to be fair) have had it pretty good when it comes to nesting. With some carefree keyboard strokes, we may wind up with CSS like this (note that I’m using Sass for brevity):
.card { ... }
.card--featured {
/* etc. */
.card__title { ... }
.card__title { ... }
}
.card__title { ... }
.card__img { ... }
In this example, we have a .card
component. When it’s a “featured” card (using the .card--featured
class), the card’s title and image needs to be styled differently. But, as we now know, the code above results in a specificity score that is inconsistent with the rest of our system.
A die-hard specificity nerd might have done this instead:
.card { ... }
.card--featured { ... }
.card__title { ... }
.card__title--featured { ... }
.card__img { ... }
.card__img--featured { ... }
That’s not so bad, right? Frankly, this is beautiful CSS.
There is a downside in the HTML though. Seasoned BEM authors are probably painfully aware of the clunky template logic that’s required to conditionally apply modifier classes to multiple elements. In this example, the HTML template needs to conditionally add the --featured
modifier class to three elements (.card
, .card__title
, and .card__img
) though probably even more in a real-world example. That’s a lot of if
statements.
The :where()
selector can help us write a lot less template logic — and fewer BEM classes to boot — without adding to the level of specificity.
.card { ... }
.card--featured { ... }
.card__title { ... }
:where(.card--featured) .card__title { ... }
.card__img { ... }
:where(.card--featured) .card__img { ... }
Here’s same thing but in Sass (note the trailing ampersands):
.card { ... }
.card--featured { ... }
.card__title {
/* etc. */
:where(.card--featured) & { ... }
}
.card__img {
/* etc. */
:where(.card--featured) & { ... }
}
Whether or not you should opt for this approach over applying modifier classes to the various child elements is a matter of personal preference. But at least :where()
gives us the choice now!
What about non-BEM HTML?
We don’t live in a perfect world. Sometimes you need to deal with HTML that is outside of your control. For instance, a third-party script that injects HTML that you need to style. That markup often isn’t written with BEM class names. In some cases those styles don’t use classes at all but IDs!
Once again, :where()
has our back. This solution is slightly hacky, as we need to reference the class of an element somewhere further up the DOM tree that we know exists.
/* ❌ specificity score: 1,0,0 */
#widget {
/* etc. */
}
/* ✅ specificity score: 0,1,0 */
.page-wrapper :where(#widget) {
/* etc. */
}
Referencing a parent element feels a little risky and restrictive though. What if that parent class changes or isn’t there for some reason? A better (but perhaps equally hacky) solution would be to use :is()
instead. Remember, the specificity of :is()
is equal to the most specific selector in its selector list.
So, instead of referencing a class we know (or hope!) exists with :where()
, as in the above example, we could reference a made up class and the <body>
tag.
/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
/* etc. */
}
The ever-present body
will help us select our #widget
element, and the presence of the .dummy-class
class inside the same :is()
gives the body
selector the same specificity score as a class (0,1,0
)… and the use of :where()
ensures the selector doesn’t get any more specific than that.
That’s it!
That’s how we can leverage the modern specificity-managing features of the :is()
and :where()
pseudo-classes alongside the specificity collision prevention that we get when writing CSS in a BEM format. And in the not too distant future, once :has()
gains Firefox support (it’s currently supported behind a flag at the time of writing) we’ll likely want to pair it with :where()
to undo its specificity.
Whether you go all-in on BEM naming or not, I hope we can agree that having consistency in selector specificity is a good thing!
BEM, that’s a blast from the past. I thought all the youngsters were using utility frameworks like Tailwind in React components these days. Nice to see BEM revisited.
Thanks Bill. BEM never went anywhere! :D
The spotlight shifted perhaps, as it tends to do in our industry.
Tailwind et al are good for certain types of projects. But for the large-scale CMS sites I often work with, I wouldn’t use anything else but BEM (with very few utility classes, fwiw).
For the last example, I know you were showing the power of
:is
, but for good measure, it might be good to also highlight you can do[id]:where(#widget)
or even[id="widget"]
, which both have a specificity of0,1,0
.Oh
[id="widget"]
is a really great point, I’d totally forgotten about that!I’m confused. Why would
.something:not(.something--special)
apply to an element with the classsomething--special
? Isn’t that literally the point of the:not()
operator?Oh yikes, I goofed up, that’s why! We’ll get that updated in the post. Good catch, thank you :)
Have been using :has for some real important stuff.. really love using these.
If we have
:where()
to prevent specifity climbing and we have selector nesting (either via SCSS or the native one coming), do we still need BEM? Perhaps we can now treat each CSS block as scoped and use simpler class names.Great questions.
BEM offers several advantages, other than just helping with specificity.
For instance, another thing I like about BEM is that it makes it clear which elements you’re directly styling. E.g. imagine a “card” component’s “title”. You could do:
But what happens when you decide they should actually be
h2
s orh4
s? You’d need to ensure the html was updated too. And it wouldn’t be easy to know which, if any, CSS to remove if you removed something like this from the HTML.(I realise that you don’t need to be using BEM to think to add a class to your h3 element – but the point is, with BEM it’s an expectation that you add it to anything you want to style).
The second part of your question was about simpler class names. I get that no one likes to type out .really__loooooong–classnames all the time. But that gets me to the next other thing I like about BEM – uniqueness. Adding the full context to each class means I can more easily search for and find their usages. For instance, I can search for
card__title
and know I’m not getting any “title”s from other components. This is why I avoid this style of Sass nesting:Good that it doesn’t increase specificity, but bad that you can’t reliably find code usages.
All that being said, you don’t need to use BEM specifically to achieve these same principles. Ultimately this will boil down to your (and your team’s) preferred way of working. If you want to have an “element”-equivalent class that looks like
.card-title
, go nuts. The main point of this article was about managing specificity. BEM is (still) widely used, so I focused on that :)I thought that this was an excellent article. I know it wasn’t your primary goal for this article, but I thought the description of the benefits of BEM was clearer than most of the other articles I’ve read on the subject.
Personally, I prefer SUIT CSS naming – we use the naming conventions, not the other SUIT tools. I prefer SUIT because the naming choices fits better with our other code (Typescript and C#). We’re not purists about it, we use combination (child or sibling) selectors to make the names easier to read and maintain in HTML – which does affect specificity.
With
:where
and:is
and Cascade Layers, which all have good support in evergreen browsers, we now have the tools for naming conventions to prioritize clean HTML and CSS design, without worrying so much about specificity. I think there could be a next gen of BEM or SUIT (or any other naming convention) that improves clarity by removing the specificity requirement that constrained the previous design.If you use phpstorm (or other jetbrains ides I assume) then the
&__title
format will work. It lets you command/ctrl click on the class name and the program is able to link you to the correct css.Not sure about other programs but there may be plugins available to help with this.
@Dan Christofi
Interesting. It’s good that phpstorm /jetbrains do this. Though I’m guessing you still can’t do an all-of-codebase search for “card__title”?
I just tried your suggestion in my editor (VS Code) and it does not work. Having said that, it doesn’t work even without the Sass nesting. I imagine there’s some add-on/setting I need to enable.
Even so, the lack of a reliable codebase text search is a deal-breaker for me, personally.
I think
.special:not(a)
has a specificity score of0,1,1
not0,2,0
. I actually got confused and went look up the doc on:not
.Whoops! I had updated that cursed code block after a different error was pointed out, but didn’t update the explanation below. All fixed!
Thanks for this post, BEM + SASS still works fine for me, too!
If you need a root selector for :where(), like in the last example:
/* ✅ specificity score: 0,1,0 */
:is(.dummy-class, body) :where(#widget) {
/* etc. */
}
I think you could use just :root
:root :where(#widget) {
/* etc. */
}
Good suggestion, Jonathan.
I only very recently (last week) learned that the
:root
selector has a specificity of 0,1,0. That’s certainly a more elegant way to do it that my hacky ideas :)I think this mention of
:is(.dummy-class, body)
might be the most CSS-tricks thing I’ve read here in quite a while, I love how you put it and how you suggest using it for this purpose.✔️ total hack
Thanks Tamm! I was feeling pretty pleased with myself about that one until someone pointed out that you could instead just do:
…which is much cleaner IMO. Noting that
:root
has the same specificity as a class, 0,1,0Good to have the detailed knowledge of BEM Format. Good to know that it is still in the race.
I am so confused about these specificity numbers. Where does one get these ordered tuples from ? In https://web.dev/learn/css/specificity/, the specificity has numbers like 1, 10, 100, etc.
So
has 11 points of specificity.
Hi Tarun!
I think the best way to become comfortable with specificity scores is to play with a calculator.
I recommend this one: https://polypane.app/css-specificity-calculator/#selector=div%3Anot(.my-class)
Note your example should be pre-loaded here.
There are other CSS specificity calculators out there, but this one is the best I’ve found because it supports all the level 4 selectors I talk about in this article (some calculators don’t).
As far as the comma-separated format is concerned, you can just read it as a plain number. For instance,
p a
has a score of
0,0,2
. You can read that as “2”.Whereas,
#thing p a
has a score of
1,0,2
. You can read that as “102”.The higher that number, the higher the specificity.