CSS Cascade Layers Vs. BEM Vs. Utility Classes: Specificity Control

About The Author

I Code. I Write. Meme Rules. Ok, but seriously, I’m a web and mobile developer who writes a lot. In my spare time, I like to build products that would … More about Victor ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

CSS can be unpredictable — and specificity is often the culprit. Victor Ayomipo breaks down how and why your styles might not behave as expected, and why understanding specificity is better than relying on !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.

Specifity tension represented by a pile of different elements
(Large preview)

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.

A chart which ilustrates different strategies on how to control or at least maintain CSS specificity
(Large preview)

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.

Legacy button vs modern button
(Large preview)

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.

Illustration for BEM methodological system
(Large preview)

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.

An illustration with a title: Avoiding specifity - one utility at a time
(Large preview)

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.

FeatureBEMUtility ClassesCascade Layers
Core IdeaNamespace componentsSingle purpose classesControl cascade order
Specificity ControlLow and flatAvoids entirelyAbsolute control due to Layer supremacy
Code ReadabilityClear structure due to namingUnclear if unfamiliar with the class namesClear if layer structure is followed
HTML VerbosityModerate class names (can get long)Many small classes that adds up quicklyNo direct impact, stays only in CSS
CSS OrganizationBy componentBy propertyBy priority order
Learning CurveRequires understanding conventionsRequires knowing the utility namesEasy to pick up, but requires a deep understanding of CSS
Tools DependencyPure CSSOften depends of third-party e.g TailwindNative CSS
Refactoring EaseHighMediumLow
Best Use CaseDesign SystemsFast buildsLegacy code or third-party codes that need overrides
Browser SupportAllAllAll (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.

Smashing Editorial (gg, yk)