Why Your CSS Won't Cooperate

Every CSS developer has been there: you write a style, it applies perfectly, then you try to override it with something new — and nothing happens. That's specificity at work. The CSS Cascade algorithm decides which declaration wins when multiple rules target the same element, and it doesn't always pick the one you wrote last.

Resorting to !important flags is tempting when styles misbehave, but it's a notoriously risky path. Once you start, you often need more !important just to override your own previous ones. Understanding how specificity actually works is the only sustainable way out of that trap.

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

The classic specificity war goes something like this: Developer A adds a .cart-button class. Later, Developer B wants that button in the sidebar with a tweak and writes .cart-button .sidebar. Now any future changes to .cart-button might lose to .cart-button .sidebar — and the arms race begins. Every new rule has to out-specify the last one.

Over the years, three main strategies have emerged to keep this problem under control: explicit naming conventions, atomic utility classes, and cascade layers. Each approaches the same problem differently, and each has its own trade-offs.

A Real-World Specificity Headache

Consider an old codebase with a selector chain that looks 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);
}

A .btn-primary class stands little chance against that selector chain. The first rule carries a specificity score of 1, 2, 1 — one ID, two classes, one element — while .btn-primary ships in at a measly 0, 1, 0. You could reach for !important, but that's a slippery slope. You could write an even more specific selector, but that's just cruel to the next developer.

#main-content .product-grid .btn-primary {
  /* edit styles directly */
}

The better option is rewriting the CSS from scratch.

Legacy button vs modern button
(Large preview)

Even modern features can trip you up. Nested CSS, for example, makes it natural to write deeply nested selectors that generate specificity scores you never intended to create:

.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
      }
    }
  }
}

That's how easily specificity creeps in. The overarching rule to live by: keep specificity as low as possible, and if your selector chains are getting complicated, rethink the structure entirely.

BEM: Structure Through Naming

Block-Element-Modifier (BEM) forces every style hierarchy to be explicit through its naming system. It's been criticized for producing "ugly" class names with double hyphens and underscores, but the payoff is predictability — all selectors stay at the same low specificity level.

/* 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 {}
Illustration for BEM methodological system
(Large preview)

Without BEM, you might write:

/* 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, the equivalent code becomes:

/* Specificity: 0, 1, 0 */
.main-nav__link {
  color: #472EFE;
  text-decoration: none;
}

/* Specificity: 0, 1, 0 */
.main-nav__link--special {
  color: #FF5733;
}

All selectors are created equal, so extending a component just means adding a new class. Need a button in .main-nav? Add .main-nav__btn. Need a disabled variant? Add .main-nav__btn--disabled. No specificity arms race, no fighting the cascade. A .card__title will never clash with a .menu__title because the naming scheme guarantees isolation.

BEM isn't without its drawbacks, though:

  • Class names can get really long.
<div class="product-carousel__slide--featured product-carousel__slide--on-sale">
  <!-- yikes -->
</div>
  • Reusability suffers. Should a button in a card be .card__button, duplicating button styles, or reuse a global .button class, breaking BEM's strict model? The methodology doesn't give you a clean answer.
  • Naming becomes a burden. Front-end developers already spend too much time deciding what to call things.

A pragmatic approach might be a hybrid system: BEM for core components, simpler classes for one-off pieces. Specificity stays low either way.

Utility Classes: Avoiding the Problem

Utility-first CSS — also called Atomic CSS — takes a completely different route. Instead of managing specificity, it sidesteps the issue entirely. Every utility class has the exact same specificity score of 0, 1, 0, and each class does one tiny thing. It's like LEGO styling: stack p-2 for padding, text-red for red text, text-center for alignment — you assemble the look you want, class by class.

<button class="bg-red-300 hover:bg-red-500 text-white py-2 px-4 rounded">
  A button
</button>
An illustration with a title: Avoiding specifity - one utility at a time
(Large preview)

Because every utility class has the same low specificity, overrides become trivial. Want more padding? Swap .p-2 for .p-4. Consider an example:

<button class="bg-orange-300 hover:bg-orange-700">
  This can be hovered
</button>

When two utility classes conflict, the CSS Cascade falls back to order of appearance — with the last matching declaration winning. The specificity part is no longer the issue, but you still have to think about the cascade's other rules.

The big drawback is aesthetic: utility-heavy markup looks ugly. For many developers, though, being able to visualize the component just from class attributes far outweighs the visual clutter. The other complaints are practical:

  • Global changes get messy. You can't change one CSS variable and have your whole brand color update if those colors are in class names scattered throughout your markup.
  • You lose parent-child relationships. Native CSS naturally expresses structure through nesting; atomic classes don't.
  • Separation of concerns suffers. HTML starts doubling as a stylesheet.
<!-- Too long -->
<div class="p-4 bg-yellow-100 border border-yellow-300 text-yellow-800 rounded">

<!-- Better? -->
<div class="alert-warning">

And if you spot repeated combinations of utility classes, your only option is to extract them into a component — which is just writing CSS again by another name. Utility classes shine at speed, letting developers style markup quickly and see results immediately, and predictability — a utility class does exactly what its name says, nothing more.

Cascade Layers: Directing the Cascade

Cascade Layers — the @layer rule — offer something BEM and utility classes don't: actual control over how the cascade behaves, regardless of specificity scores. You group your styles and declare an order, and the browser respects that order no matter the specificity of the rules inside each layer.

Look at an independent set of rules where one has an ID selector:

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 */

Here, #button wins because IDs outweigh classes in specificity. But with @layer, you can reorganize priorities:

@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 */
  }
}

The .button class wins because the components layer takes precedence over the layer containing #button. And remember: variables work inside layer blocks too.

@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 */
  }
}

You just overrode an ID selector with a class — no !important needed. There are caveats, though:

  • Specificity still matters within a group. Layer order decides between layers, but the normal specificity rules still apply inside each layer.
  • !important behaves differently, working in reverse within @layer.
  • @layer groups by property declarations, not selectors.
  • It's abusable. Nothing stops you from declaring 20+ layers that grow into a monstrosity.

A quick side-by-side comparison helps cut through the trade-offs:

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)

Each strategy has its own sweet spot. BEM works where a team needs consistency and shared conventions, or where styles between components must not leak. Utility classes dominate for rapid prototyping, MVPs and component-based JavaScript frameworks. Cascade Layers excel with legacy codebases, integrating third-party style sources, or large scale applications built for the long haul.

They Aren't Mutually Exclusive

BEM and utility classes are methodologies — frameworks for naming and organizing styles. Cascade Layers, however, are native to the CSS Cascade architecture. Layers can act as an orchestrator that works alongside either approach, wrapping whole systems of classes into an intentional hierarchy.

/* 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;
  }
}

Mixing BEM with utility classes, on the other hand, tends to create more problems than it solves:

<!-- 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>

In practice, most modern utility frameworks already use @layer internally, so these two methods have converged organically. Whether you prefer the structure of BEM or the speed of utilities comes down to personal taste and project needs. Multiple routes lead to the same destination.

No Single Winner: The Case for Layering Strategies

So, is there a definitive “best” approach for controlling specificity among BEM, utility classes, and CSS Cascade Layers? The short answer is no — not because they compete, but because they operate in different realms.

CSS Cascade Layers are arguably the most powerful CSS feature to arrive in years. Unlike BEM or utility classes, which are stylistic conventions or organizational strategies, Cascade Layers are a native part of the CSS feature set. This distinction matters: you can’t pit a language feature against a naming methodology as if they were the same tool.

The more promising path is to combine strategies rather than choose between them. Both BEM and utility classes can work exceptionally well when paired with Cascade Layers. The principle is consistent: maintain low specificity in your selectors and delegate priority management to the layer order.

This hybrid approach gives you the readability and structure of a naming system, whether you prefer the block-centric granularity of BEM or the composability of utilities, while offloading the tricky work of resolve conflicts to the Cascade’s own machinery.

Smashing Editorial