Why we bet on CSS Cascade Layers (and never wrote !important again)
Specificity wars are a design-system killer. Layers flatten the cascade, make overrides trivial, and let contributors ship without reviewer fatigue.
For the first eighteen months of RenDS, every fourth pull request had the same
review comment: "please remove the !important." The team
wasn't being lazy — they were being pragmatic. A token override needed to win.
A theme override needed to win harder. Specificity didn't care about intent.
Then we adopted CSS Cascade Layers.
Within a sprint, the comment disappeared from every PR. We removed every
!important in the codebase. We onboarded two contributors who'd
never touched a design system and they shipped components in their first week.
What was actually broken
The cascade was doing exactly what the spec says it does. The problem was that the spec wasn't enough on its own. Three things kept colliding:
- Reset styles — broad selectors that shouldn't lose to component CSS, but kept doing exactly that.
- Component CSS — moderate specificity, written by everyone, hard to predict.
- Theme overrides — needed to win against components, but couldn't always.
The fix was either to declare a strict ordering convention (and police it forever) or to escalate specificity (and never come back down). We chose neither. We chose layers.
Layers in three sentences
A layer is a named bucket of styles. Layers cascade in declaration order, and a rule in a later layer always wins over the same rule in an earlier layer — regardless of selector specificity. That's the whole feature.
@layer reset, base, tokens, components, themes, overrides;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
}
@layer components {
.ren-btn { background: var(--color-accent); }
}
@layer themes {
/* Wins over .ren-btn even with lower specificity */
[data-theme="forest"] .ren-btn { background: var(--color-emerald); }
}
The order line at the top is the entire policy. Everything else flows from it.
The clarifying property of layers: rule ordering becomes a decision you make once, in one place, instead of a war that's fought in every PR.
How RenDS uses them
We declare the layer order once in index.css. Every other file
opens into a named layer — never the implicit one. The order is:
reset— minimal box-sizing, default font, link color reset.base— the classless layer (raw<button>,<input>,<table>) so unstyled HTML already looks right.tokens— primitive, semantic, and component custom properties.components— every.ren-*class.themes— appearance overrides via[data-theme].overrides— the user's last resort. Always wins.
Anything outside a layer would win over everything inside one (that's how the cascade works) so the only place you'll see un-layered CSS is in test fixtures.
What we got back
1. Code review without specificity arguments.
Reviewers stopped counting selectors. The question stopped being "will this win?" and started being "does this belong in this layer?"
2. Themes that actually theme.
Before layers, a theme override needed at least the specificity of the original
rule plus a context selector. Now [data-theme] .ren-btn beats
.ren-btn-primary.ren-btn-lg:hover simply because it's in a later
layer.
3. Newcomers ship faster.
New contributors don't need a mental model of the codebase's specificity topography. They learn the six layer names. Then they write CSS.
Layers turned the cascade from a battlefield into a spreadsheet. We open the spreadsheet, find the row, write our CSS.
The honest caveats
Layers have great browser support today (Chromium, Firefox, and Safari all shipped them in 2022) but if you need to support older browsers you'll need a fallback strategy. We chose the easy path: RenDS targets evergreen browsers and documents the fact.
And layers don't replace good architecture. You can still write tangled CSS
inside a layer — the layer just bounds the chaos. We pair them with a strict
BEM-ish naming convention (.ren-card, .ren-card-header)
so even within a layer, intent is obvious.
What's next
Cascade Layers solved the easiest hard problem in our CSS: order. The
next one is scope. The @scope at-rule is shipping in
browsers as I write this, and we're already prototyping a layer-aware
component scope. If it works, the next post in this series will be about how
we deleted half our specificity hacks all over again.