CSS Cascade Layers Vs. BEM Vs. Utility Classes: Specificity Control
!important
flags.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:
- BEM: tries to simplify specificity by being explicit.
- Utility-first CSS: tries to bypass specificity by keeping it all atomic.
- CSS Cascade Layers: manage specificity by organizing styles in layered groups.
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:
- I could use
!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. - I could try going more specific, but personally, that’s just being cruel to the next developer (who might even be me).
- I could change the styles of the existing code, but that’s adding to the specificity problem:
#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.

How BEM Handles Specificity
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.
Where BEM Falls Short
I like the idea of BEM, but it is not perfect, and a lot of people noticed it:
- The class names can get really long.
<div class="product-carousel__slide--featured product-carousel__slide--on-sale">
<!-- yikes -->
</div>
- Reusability might not be prioritized, which somewhat contradicts the native CSS ideology. Should a button inside a card be
.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. - One of the core pains in software development starts becoming a reality — naming things. I’m sure you know the frustration of that already.
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.

How Utility Classes Handle Specificity
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.
Utility Class Trade-Offs
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:
- If your brand color changes, which is a global change, and you’re deep in the codebase, you can’t just change one and have others follow like native CSS.
- The parent-child relationship that happens naturally in native CSS is out the window due to how atomic utility classes behave.
- Some argue the HTML part should be left as markup and the CSS part for styling. Because now, there’s more markup to scan, and if you decide to clean up:
<!-- 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:
- Speed
Writing the markup, styling it, and seeing the result swiftly. - Predictability
A utility class does exactly what it says it does.
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.
Cascade Layers Nuances
Here are some things that are worth calling out when we’re talking about CSS Cascade Layers:
- Specificity is still part of the game.
!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.
Comparing All Three
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:
- BEM is best when:
- There’s a clear design system that needs to be consistent,
- There’s a team with different philosophies about CSS (BEM can be the middle ground), and
- Styles are less likely to leak between components.
- Utility classes work best when:
- You need to build fast, like prototypes or MVPs, and
- Using a component-based JavaScript framework like React.
- Cascade Layers are most effective when:
- Working on legacy codebases where you need full specificity control,
- You need to integrate third-party libraries or styles from different sources, and
- Working on a large, complex application or projects with long-term maintenance.
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.
