Tame Specificity with Cascade Layers
29 Oct 2026The !important arms race happens because specificity is a blunt instrument. Two rules fight over a button color, one side wins with !important, six months later someone needs to override that and adds their own !important, and now you’ve got a stylesheet where half the rules are marked urgent. Cascade layers fix the actual problem: they let you control which group of styles wins, independent of how specific any individual selector is.
The core idea
@layer lets you name groups of rules and set their priority explicitly, in the order you declare the layer names — not in source order, not by specificity:
@layer reset, base, components, utilities;
@layer reset {
* { margin: 0; padding: 0; }
}
@layer components {
.button { background: blue; padding: 0.5rem 1rem; }
}
@layer utilities {
.bg-red { background: red; }
}
Layers declared later win over layers declared earlier, regardless of selector specificity within them. So .bg-red in utilities beats .button in components, even though .button is arguably “more specific” in the class sense — because utilities is the later layer. That’s the actual win: a one-class utility can reliably beat a deeply nested component selector without a single !important.
Where it actually helps
The classic pain point is a component library (or Tailwind-style utilities) fighting your own CSS. Put third-party or reset CSS in an early layer, your components in the middle, and escape-hatch utility classes last:
@layer reset, vendor, components, utilities;
Anything outside a layer — unlayered CSS — beats every layered rule, no matter its specificity. That’s the one rule to memorize: unlayered always wins over layered. So if a third-party stylesheet isn’t layered, wrapping your own overrides in @layer won’t beat it; you’d need to import the vendor CSS into a layer too:
@import url("vendor.css") layer(vendor);
How the cascade actually resolves now
flowchart TD
A["Two conflicting declarations"] --> B{"Both !important?"}
B -->|Yes| C["Higher layer wins (reverse order)"]
B -->|No, neither important| D{"Is one unlayered?"}
D -->|Yes| E["Unlayered rule wins"]
D -->|No, both layered| F["Later-declared layer wins"]
C --> G["Tie in importance/layer?"]
F --> G
G --> H["Higher specificity wins"]
H --> I["Later source order wins"]
Layer order is the tiebreaker that runs before specificity, not after. That’s the whole mental model shift: stop thinking “which selector is stronger” and start thinking “which layer wins.”
Practical rules
- Declare all your layer names up front in one
@layer reset, base, components, utilities;statement, even before any of them have rules. This fixes the order regardless of which layer gets filled in first as your CSS grows. - Unlayered CSS always beats layered CSS. If you’re migrating an existing stylesheet, either layer everything or layer nothing for a given source — don’t half-migrate one file.
!importantstill works inside layers, and it inverts the layer order for importance conflicts specifically. In practice: avoid!importantentirely once you have layers; you don’t need it anymore.
Verdict
Four layers — reset, base, components, utilities — covers most sites. Stop reaching for !important to win a cascade fight; reach for a layer instead. It’s the tool actually built for that problem.