Using @layer to make CSS precedence a declared order, not a specificity fight
Runaway styles come from everyone solving problems by adding specificity. Cascade layers make the order explicit: between layers only order matters, and specificity applies within a layer.
Every long-lived project eventually grows an !important. Not from laziness, but because solving problems by adding specificity cannot scale: you add one class to beat the old rule, then two classes to beat that one.
Cascade layers take a different route: instead of competing on specificity, you declare the order of a few groups up front.
The basics
@layer reset, base, components, utilities;
@layer base {
a { color: navy; }
}
@layer utilities {
.link-muted { color: gray; }
}
As long as utilities comes after base, .link-muted always wins — even though base uses the more specific selector a.
Three rules cover it:
- The order declaration decides everything. A layer’s priority comes from where it first appears, not from what is inside it
- Between layers, specificity is not consulted at all
- Inside a layer, specificity works exactly as before
The third matters: @layer does not abolish specificity, it confines it to each layer.
Unlayered styles are the strongest
Rules outside any @layer sit above every layer:
@layer base {
.x { color: navy; }
}
.x { color: red; } /* unlayered, wins */
That property makes migration practical: put new code in layers and leave old code outside while both coexist. Once the old code has moved, the outside is empty.
Beware, though, because this is where @layer bites hardest: on the day you introduce layers, all unlayered legacy styles suddenly override your new ones. Either move everything at once, or start with the bottom-most layers (reset, base typography).
Which problems it actually solves
| Problem | The old fix | With layers |
|---|---|---|
| Utility classes lose to component classes | more specificity, !important |
put utilities in the last layer |
| Third-party styles intrude | copy them locally to edit | put third-party first |
| Theme overrides are hard to write | hunt for selectors | a dedicated theme layer, last |
| Order depends on import position | a comment asking nicely | one @layer line fixes it |
That last row is the biggest win. Load order used to decide the outcome, and load order lived in build config; now it is a visible line of source.
Nesting
Layers can contain layers:
@layer components {
@layer form, button;
}
Sub-layer order is declared the same way. That expresses ordering within a component, but keep it to two levels — beyond that nobody remembers the order and you are guessing again.
When not to use it
- Small sites with little CSS: the payoff does not cover the learning cost
- Browsers that do not support it:
@layerblocks are dropped entirely (not degraded), so weigh the risk - Mixing with lots of
!important:!importantreverses the layer order (earlier layers win), a counter-intuitive rule that makes everything opaque
That last point deserves emphasis: inside !important, the layer declared first wins. Introducing layers is therefore a good moment to clear out !important; mixing the two is harder to maintain than either alone.
Cascade layers do not make CSS simpler. They swap implicit specificity arithmetic for one explicit declaration. Visible rules always outlive calculated ones.

Comments
…