Source: Articles on Smashing Magazine — For Web Designers And Developers | Read More
CSS is wild, really wild. And tricky. But let’s talk specifically about specificity.
When writing CSS, it’s close to impossible that you haven’t faced the frustration of styles not applying as expected — that’s specificity. You applied a style, it worked, and later, you try to override it with a different style and… nothing, it just ignores you. Again, specificity.
Sure, there’s the option of resorting to !important
flags, but like all developers before us, it’s always risky and discouraged. It’s way better to fully understand specificity than go down that route because otherwise you wind up fighting your own important styles.
Specificity 101
Lots of developers understand the concept of specificity in different ways.
The core idea of specificity is that the CSS Cascade algorithm used by browsers determines which style declaration is applied when two or more rules match the same element.
Think about it. As a project expands, so do the specificity challenges. Let’s say Developer A adds .cart-button
, then maybe the button style looks good to be used on the sidebar, but with a little tweak. Then, later, Developer B adds .cart-button .sidebar
, and from there, any future changes applied to .cart-button
might get overridden by .cart-button .sidebar
, and just like that, the specificity war begins.
I’ve written CSS long enough to witness different strategies that developers have used to manage the specificity battles that come with CSS.
/* Traditional approach */
#header .nav li a.active color: blue;
/* BEM approach */
.header__nav-item--active color: blue;
/* Utility classes approach */
.text-blue color: blue;
/* Cascade Layers approach */
@layer components
.nav-link.active color: blue;
All these methods reflect different strategies on how to control or at least maintain CSS specificity:
We’re going to put all three side by side and look at how they handle specificity.
My Relationship With Specificity
I actually used to think that I got the whole picture of CSS specificity. Like the usual inline greater than ID greater than class greater than tag. But, reading the MDN docs on how the CSS Cascade truly works was an eye-opener.
There’s a code I worked on in an old codebase provided by a client, which looked something like this:
/* Legacy code */
#main-content .product-grid button.add-to-cart
background-color: #3a86ff;
color: white;
padding: 10px 15px;
border-radius: 4px;
/* 100 lines of other code here */
/* My new CSS */
.btn-primary
background-color: #4361ee; /* New brand color */
color: white;
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
Looking at this code, no way that the .btn-primary
class stands a chance against whatever specificity chain of selectors was previously written. As far as specification goes, CSS gives the first selector a specificity score of 1, 2, 1
: one point for the ID, two points for the two classes, and one point for the element selector. Meanwhile, the second selector is scored as 0, 1, 0
since it only consists of a single class selector.
Sure, I had some options:
!important
on the properties in .btn-primary
to override the ones declared in the stronger selector, but the moment that happens, be prepared to use it everywhere. So, I’d rather avoid it.#main-content .product-grid .btn-primary
/* edit styles directly */
Eventually, I ended up writing the whole CSS from scratch.
When nesting was introduced, I tried it to control specificity that way:
.profile-widget
// ... other styles
.header
// ... header styles
.user-avatar
border: 2px solid blue;
&.is-admin
border-color: gold; // This becomes .profile-widget .header .user-avatar.is-admin
And just like that, I have unintentionally created high-specificity rules. That’s how easily and naturally we can drift toward specificity complexities.
So, to save myself a lot of these issues, I have one principle I always abide by: keep specificity as low as possible. And if the selector complexity is becoming a complex chain, I rethink the whole thing.
BEM: The OG System
The Block-Element-Modifier (BEM, for short) has been around the block (pun intended) for a long time. It is a methodological system for writing CSS that forces you to make every style hierarchy explicit.
/* Block */
.panel
/* Element that depends on the Block */
.panel__header
.panel__content
.panel__footer
/* Modifier that changes the style of the Block */
.panel--highlighted
.panel__button--secondary
When I first experienced BEM, I thought it was amazing, despite contrary opinions that it looked ugly. I had no problems with the double hyphens or underscores because they made my CSS predictable and simplified.
Take a look at these examples. Without BEM:
/* Specificity: 0, 3, 0 */
.site-header .main-nav .nav-link
color: #472EFE;
text-decoration: none;
/* Specificity: 0, 2, 0 */
.nav-link.special
color: #FF5733;
With BEM:
/* Specificity: 0, 1, 0 */
.main-nav__link
color: #472EFE;
text-decoration: none;
/* Specificity: 0, 1, 0 */
.main-nav__link--special
color: #FF5733;
You see how BEM makes the code look predictable as all selectors are created equal, thus making the code easier to maintain and extend. And if I want to add a button to .main-nav
, I just add .main-nav__btn
, and if I need a disabled button (modifier), .main-nav__btn--disabled
. Specificity is low, as I don’t have to increase it or fight the cascade; I just write a new class.
BEM’s naming principle made sure components lived in isolation, which, for a part of CSS, the specificity part, it worked, i.e, .card__title
class will never accidentally clash with a .menu__title
class.
I like the idea of BEM, but it is not perfect, and a lot of people noticed it:
<div class="product-carousel__slide--featured product-carousel__slide--on-sale">
<!-- yikes -->
</div>
.card__button
or reuse a global .button
class? With the former, styles are being duplicated, and with the latter, the BEM strict model is being broken.BEM is good, but sometimes you may need to be flexible with it. A hybrid system (maybe using BEM for core components but simpler classes elsewhere) can still keep specificity as low as needed.
/* Base button without BEM */
.button
/* Button styles */
/* Component-specific button with BEM */
.card__footer .button
/* Minor overrides */
Utility Classes: Specificity By Avoidance
This is also called Atomic CSS. And in its entirety, it avoids specificity.
<button class="bg-red-300 hover:bg-red-500 text-white py-2 px-4 rounded">
A button
</button>
The idea behind utility-first classes is that every utility class has the same specificity, which is one class selector. Each class is a tiny CSS property with a single purpose.
p-2
? Padding, nothing more. text-red
? Color red for text. text-center
? Text alignment. It’s like how LEGOs work, but for styling. You stack classes on top of each other until you get your desired appearance.
Utility classes do not solve specificity, but rather, they take the BEM ideology of low specificity to the extreme. Almost all utility classes have the same lowest possible specificity level of (0
, 1
, 0
). And because of this, overrides become easy; if more padding is needed, bump .p-2
to .p-4
.
Another example:
<button class="bg-orange-300 hover:bg-orange-700">
This can be hovered
</button>
If another class, hover:bg-red-500
, is added, the order matters for CSS to determine which to use. So, even though the utility classes avoid specificity, the other parts of the CSS Cascade come in, which is the order of appearance, with the last matching selector declared being the winner.
The most common issue with utility classes is that they make the code look ugly. And frankly, I agree. But being able to picture what a component looks like without seeing it rendered is just priceless.
There’s also the argument of reusability, that you repeat yourself every single time. But once one finds a repetition happening, just turn that part into a reusable component. It also has its genuine limitations when it comes to specificity:
<!-- Too long -->
<div class="p-4 bg-yellow-100 border border-yellow-300 text-yellow-800 rounded">
<!-- Better? -->
<div class="alert-warning">
Just like that, we’ve ended up writing CSS. Circle of life.
In my experience with utility classes, they work best for:
Cascade Layers: Specificity By Design
Now, this is where it gets interesting. BEM offers structure, utility classes gain speed, and CSS Cascade Layers give us something paramount: control.
Anyways, Cascade Layers (@layers
) groups styles and declares what order the groups should be, regardless of the specificity scores of those rules.
Looking at a set of independent rulesets:
button
background-color: orange; /* Specificity: 0, 0, 1 */
.button
background-color: blue; //* Specificity: 0, 1, 0*/
#button
background-color: red; /* Specificity: 1, 0, 0 */
/* No matter what, the button is red */
But with @layer
, let’s say, I want to prioritize the .button
class selector. I can shape how the specificity order should go:
@layer utilities, defaults, components;
@layer defaults
button
background-color: orange; /* Specificity: 0, 0, 1 */
@layer components
.button
background-color: blue; //* Specificity: 0, 1, 0*/
@layer utilities
#button
background-color: red; /* Specificity: 1, 0, 0 */
Due to how @layer
works, .button
would win because the components
layer is the highest priority, even though #button
has higher specificity. Thus, before CSS could even check the usual specificity rules, the layer order would first be respected.
You just have to respect the folks over at W3C, because now one can purposely override an ID selector with a simple class, without even using !important
. Fascinating.
Here are some things that are worth calling out when we’re talking about CSS Cascade Layers:
!important
acts differently than expected in @layer
(they work in reverse!).@layers
aren’t selector-specific but rather style-property-specific.@layer base
.button
background-color: blue;
color: white;
@layer theme
.button
background-color: red;
/* No color property here, so white from base layer still applies */
@layer
can easily be abused. I’m sure there’s a developer out there with over 20+ layer declarations that’s grown into a monstrosity.Now, for the TL;DR folks out there, here’s a side-by-side comparison of the three: BEM, utility classes, and CSS Cascade Layers.
Feature | BEM | Utility Classes | Cascade Layers |
---|---|---|---|
Core Idea | Namespace components | Single purpose classes | Control cascade order |
Specificity Control | Low and flat | Avoids entirely | Absolute control due to Layer supremacy |
Code Readability | Clear structure due to naming | Unclear if unfamiliar with the class names | Clear if layer structure is followed |
HTML Verbosity | Moderate class names (can get long) | Many small classes that adds up quickly | No direct impact, stays only in CSS |
CSS Organization | By component | By property | By priority order |
Learning Curve | Requires understanding conventions | Requires knowing the utility names | Easy to pick up, but requires a deep understanding of CSS |
Tools Dependency | Pure CSS | Often depends of third-party e.g Tailwind | Native CSS |
Refactoring Ease | High | Medium | Low |
Best Use Case | Design Systems | Fast builds | Legacy code or third-party codes that need overrides |
Browser Support | All | All | All (except IE) |
Among the three, each has its sweet spot:
If I had to choose or rank them, I’d go for utility classes with Cascade Layers over using BEM. But that’s just me!
Where They Intersect (How They Can Work Together)
Among the three, Cascade Layers should be seen as an orchestrator, as it can work with the other two strategies. @layer
is a fundamental tenet of the CSS Cascade’s architecture, unlike BEM and utility classes, which are methodologies for controlling the Cascade’s behavior.
/* Cascade Layers + BEM */
@layer components
.card__title
font-size: 1.5rem;
font-weight: bold;
/* Cascade Layers + Utility Classes */
@layer utilities
.text-xl
font-size: 1.25rem;
.font-bold
font-weight: 700;
On the other hand, using BEM with utility classes would just end up clashing:
<!-- This feels wrong -->
<div class="card__container p-4 flex items-center">
<p class="card__title text-xl font-bold">Something seems wrong</p>
</div>
I’m putting all my cards on the table: I’m a utility-first developer. And most utility class frameworks use @layer
behind the scenes (e.g., Tailwind). So, those two are already together in the bag.
But, do I dislike BEM? Not at all! I’ve used it a lot and still would, if necessary. I just find naming things to be an exhausting exercise.
That said, we’re all different, and you might have opposing thoughts about what you think feels best. It truly doesn’t matter, and that’s the beauty of this web development space. Multiple routes can lead to the same destination.
Conclusion
So, when it comes to comparing BEM, utility classes, and CSS Cascade Layers, is there a true “winning” approach for controlling specificity in the Cascade?
First of all, CSS Cascade Layers are arguably the most powerful CSS feature that we’ve gotten in years. They shouldn’t be confused with BEM or utility classes, which are strategies rather than part of the CSS feature set.
That’s why I like the idea of combining either BEM with Cascade Layers or utility classes with Cascade Layers. Either way, the idea is to keep specificity low and leverage Cascade Layers to set priorities on those styles.