Skip to content
basecomponentsutilities!

CSS Cascade Layers: The !important Gotcha Nobody Warns You About

·TechCSSTailwind CSSWeb Standards

Every explanation of @layer follows the same script: here’s a specificity war, here’s @layer fixing it, the end. That’s true as far as it goes, but it skips the part of the spec that actually trips people up in production, which is what happens the moment !important enters a layered stylesheet. It doesn’t behave the way you’d guess from the non-layered cascade you already know. It inverts.

I want to walk through the mechanics precisely, because “precisely” matters here — get the direction of that inversion wrong and you’ll ship a fix that makes the bug worse.

The problem @layer actually solves

Specificity wars happen because CSS resolves conflicting declarations by comparing selector weight, not by comparing intent. A component library ships .btn { color: blue } with a class selector. You write a utility class also using a class selector. Whoever’s rule appears later in source order wins — until someone adds an ID selector or nests things one level deeper, and now the “later” rule loses anyway. Nobody’s changed what they meant; the cascade just picked a winner based on syntax shape.

/* vendor.css, loaded first */
.card .title { color: navy; }

/* your utilities.css, loaded second */
.text-red { color: red; }

.card .title has specificity (0,2,0). .text-red has (0,1,0). Load order doesn’t matter — the higher-specificity rule wins regardless of which file comes last. This is the exact failure mode utility-first CSS runs into constantly: a one-class utility trying to override a nested component selector and losing on specificity even though it was clearly meant to be the last word.

The old fix was !important everywhere, or selector-inflating hacks (.text-red.text-red), both of which just relocate the war rather than end it.

What @layer actually changes

MDN’s cascade layers page states the mechanism plainly: once you’ve declared layer order, specificity and source order stop mattering between layers. Priority is decided entirely by which layer a declaration lives in, full stop. A one-class selector in a later layer beats a triple-nested ID selector in an earlier layer, every time.

@layer base, utilities;

@layer base {
  .card .title { color: navy; }
}

@layer utilities {
  .text-red { color: red; }
}

Text is red. .card .title is more specific and it doesn’t matter, because base was declared before utilities. You’ve moved the fight from “whose selector is heavier” to “which layer comes later,” and layer order is something you control explicitly at the top of the file, not something that falls out of how deeply someone nested a selector three files ago.

One more rule from the spec that matters here: unlayered styles — any CSS not inside an @layer block at all — sit in an implicit final layer for normal declarations, and that implicit layer has the highest priority of all. So a stray unlayered override anywhere in your build can silently beat everything inside your carefully ordered layers. If you’re adopting @layer incrementally, this is usually where the first confusing bug comes from: some third-party CSS that never got wrapped in a layer just wins by default.

Nested layers work by dot-joining names (@layer framework.layout { ... }), and anonymous @layer { ... } blocks work the same way for ordering purposes but can never be appended to later — you get one shot to add rules to an anonymous layer, then it’s sealed.

The part everyone gets backwards: !important inside layers

Here’s the actual crux, and it’s worth quoting the spec directly rather than paraphrasing, because paraphrasing is how the confusion spreads in the first place. From the CSS Cascading and Layering Level 1 spec:

For normal rules, “the declaration whose cascade layer is last wins.” For important rules, “the declaration whose cascade layer is first wins.”

Read that twice. For normal declarations, later layer wins. For !important declarations, earlier layer wins. It’s a full inversion, not a special case bolted on top — the spec treats !important as reversing the entire priority order of layers, including how unlayered styles are treated. Unlayered normal styles have the highest priority; unlayered !important styles have the lowest.

Why design it this way at all? The intent is defensive: !important is supposed to represent an emergency override, usually something a framework or reset stylesheet applies early, expecting downstream authors not to be able to casually clobber it. If later layers could still out-rank earlier !important declarations, !important would be nearly powerless inside a layered stylesheet — anyone shipping a later layer could always beat it, same as normal rules, and the “important” flag would stop meaning anything in the presence of layers. Reversing the order preserves the idea that an early, deliberate !important should be hard to override by code that comes after it.

In practice:

@layer base, utilities;

@layer base {
  .title { color: navy !important; }
}

@layer utilities {
  .text-red { color: red !important; }
}

Text is navy. Not red. If you only skimmed the “later layer wins” rule from the first half of this post and reflexively expected .text-red to win because utilities comes later, you just reproduced the most common @layer bug in the wild. The moment !important shows up in either layer, flip your mental model.

This is genuinely the detail that separates “I read one blog post about @layer” from “I understand what will happen when these two stylesheets collide.” Most write-ups never mention it because the happy path — normal declarations, no !important — is the 90% case. But !important is exactly the tool people reach for when they’re already losing a specificity war, which means it shows up disproportionately in the messy legacy CSS you’re trying to layer around.

Where Tailwind v4 fits in

This isn’t an abstract spec curiosity for anyone shipping Tailwind. Tailwind CSS v4 generates its output wrapped in native cascade layers. The framework’s own v4 announcement shows the exact structure:

@layer theme, base, components, utilities;

theme holds the CSS custom properties from @theme, base holds Preflight and any base-layer resets, components is for custom component classes, and utilities — declared last — holds the generated utility classes, including your own custom ones added via @layer utilities. Because utilities is the last-declared layer, every utility class in that layer outranks every selector in base or components, regardless of specificity. The exact problem from the first section of this post — a component library’s nested selector beating a one-class utility — is solved structurally, at the layer level, instead of by hoping your build tool controls source order correctly or piling on !important.

I hit a version of this firsthand rebuilding this site on Tailwind v4: pull in any third-party component CSS that isn’t itself layer-aware, and it lands in that unlayered “implicit final layer” MDN describes, which for normal declarations outranks everything Tailwind generates — theme, base, components, utilities, all of it. Tailwind’s utilities can’t win against it just by being utilities anymore. You have to either wrap the third-party CSS in its own @layer (giving it an explicit, lower-priority slot) or accept that unlayered CSS is effectively privileged. It’s a good reminder that @layer isn’t magic that fixes specificity everywhere — it only governs precedence between things that opted into a layer.

The mental model worth keeping

Two rules, and they’re opposites:

Everything else — nesting, anonymous layers, how Tailwind structures its output — is a consequence of those two rules applied consistently. Once that inversion is wired into how you read a stylesheet, @layer stops being a trick for winning specificity fights and starts being what it actually is: an explicit, declared priority order that doesn’t care about selector shape at all.

Sources