Skip to content
C Codeloom
HTML & CSS

CSS Cascade Layers and Specificity: A Calmer Mental Model

Stop fighting !important. Learn how the CSS cascade, specificity, and the new @layer rule combine to give you predictable, maintainable styles.

·5 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How the CSS cascade decides which rule wins
  • How specificity is actually counted
  • What @layer does and why it changes everything
  • Where !important and inline styles really sit
  • How to organize a real stylesheet with layers

Prerequisites

  • Basic CSS familiarity

What and Why

When two CSS rules try to style the same property on the same element, the browser has to pick one. That selection process is the cascade. Most “why is my CSS not applying” questions are really cascade questions.

For decades, specificity was the main lever, and projects ended up in escalating !important wars. Cascade layers (@layer, shipped in all major browsers since 2022) give you a clean way to declare ordering up front, without specificity gymnastics.

Mental Model

When the browser resolves a property, it walks several tiers in order. Within each tier, specificity and source order break ties.

1. transitions
2. !important user-agent
3. !important user
4. !important author
5. animations
6. normal author          <-- @layer ordering applies here
7. normal user
8. normal user-agent
9. revert / unset / initial
The cascade, simplified

Within the “normal author” tier, layers are evaluated in declaration order: rules in earlier layers lose to rules in later layers, regardless of selector specificity. Unlayered styles win over any layered style by default.

Specificity inside a layer is still counted as four numbers (inline, IDs, classes/attrs/pseudo-classes, elements/pseudo-elements). Higher wins; ties go to whatever appears later.

Hands-on Example

Without layers, a third-party CSS file can be hard to override because its selectors are too specific. With layers, you declare an order:

@layer reset, base, components, utilities;

@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
}

@layer base {
  body { font-family: system-ui, sans-serif; color: #111; }
  a { color: #06f; }
}

@layer components {
  .btn {
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    background: #06f;
    color: #fff;
  }
}

@layer utilities {
  .text-center { text-align: center; }
  .mt-2 { margin-top: 0.5rem; }
}

Now any rule in utilities beats any rule in components, even if the component selector is more specific. You no longer need !important or hyper-specific selectors to make a utility “stick”.

Import a third-party stylesheet into its own layer to keep it tame:

@import url("vendor.css") layer(vendor);
@layer vendor, base, components, utilities;

By placing vendor first in the declared order, your own layers automatically win.

Specificity inside a layer:

@layer components {
  .btn          { color: white; }              /* (0,1,0) */
  .card .btn    { color: gray; }               /* (0,2,0) wins inside layer */
}

But across layers, declaration order of layers dominates:

@layer a, b;

@layer b {
  .btn { color: red; }                          /* (0,1,0) */
}
@layer a {
  #main .btn { color: blue; }                   /* (1,1,0) but in earlier layer */
}

Inside @layer b, color: red wins. Specificity does not save the blue rule because layer a was declared first and therefore loses to layer b.

Unlayered styles win against any layered style:

@layer components { .btn { color: red; } }

.btn { color: green; }   /* not in any layer - wins */

This is convenient for quick overrides but dangerous if abused; treat unlayered styles as a small escape hatch.

Common Pitfalls

Declaring layers after using them. The first occurrence of @layer name, name2; sets the order. If you write a @layer components {...} block first and only later say @layer utilities, components;, the order may not be what you expected. Put your layer order declaration at the very top.

Mixing layered and unlayered styles without intent. Unlayered wins, so a stray rule outside any layer can quietly override the layer system you just built.

Reaching for !important to escape a layer. !important flips a rule into a higher tier where layer order is reversed (earlier layers win). It works, but it is a confusing tool. Prefer fixing the layer order or moving the rule.

Assuming inline styles always win. They beat author styles in the normal tier but lose to !important author styles. Inline style="..." is also annoying to override; avoid where possible.

Confusing the specificity of :is() and :where(). :is() takes the specificity of its most specific argument; :where() is always zero. :where() is great for low-specificity defaults.

Practical Tips

Start every project with one short prelude:

@layer reset, base, components, utilities, overrides;

This guarantees predictable ordering before any other rule arrives.

Wrap component libraries in their own layer so app code can override them without specificity wars.

Use :where() to give defaults that anything else can override:

:where(a) { color: var(--link); }

Now even a single class selector wins, no !important required.

Audit !important periodically. Each one is a small admission that the cascade got away from you. With layers, most can be removed.

Tools like the browser dev tools’ “Computed” tab now show layer names next to rules. Use them when chasing why a property is not applying.

Wrap-up

The cascade picks a winner by walking tiers, then layers, then specificity, then source order. Specificity used to be the only knob; @layer adds a much better one. Declare your layer order, group reset, base, components, and utilities, and you can stop typing !important and start trusting your stylesheet again.