Taming the Cascade With BEM and Modern CSS Selectors
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
:focus pseudo classes. Those have a specificity score of
0,2,0. Another is pseudo elements — like
::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. 🤓
: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.
Lemme stick one of those pseudo-classes in my BEM and…
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 this same example, expanded:
Even though the second selector is last in the source order, the first selector’s higher specificity (
0,2,0) 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.
Remember what I was saying about
:where() and the fact that its specificity is zero? We can use that to our advantage:
The first part of this selector (
.something) gets its usual specificity score of
:where() — and everything inside it — has a specificity of
0, which does not increase the specificity of the selector any further.
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):
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:
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__img) though probably even more in a real-world example. That’s a lot of
:where() selector can help us write a lot less template logic — and fewer BEM classes to boot — without adding to the level of specificity.
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!
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!
: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.
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 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.
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!
If you need help creating a digital marketing strategy for your business, don’t hesitate to contact one of Digidude’s consultants.