Lesson 16 of 37
Cascade Layers
@layer decides the order in which groups of CSS rules apply — reset, design system and app code each live in a layer whose priority is set once, not fought with specificity bumps.
Every real codebase eventually hits the same situation: a button in the design system was styled by one team, the marketing page's hero button was styled by another team with a slightly more specific selector to "win," and three months later nobody is sure which selector a page is picking up without opening the browser inspector. The underlying problem isn't bad CSS — it's that specificity has been the only tool available for deciding "which group of rules wins when they conflict," and specificity is a local, ad-hoc game.
Cascade layers (@layer) are the tool for solving that at the
group level: declare the order between whole bodies of CSS once,
and stop fighting about it.
The cascade, with one new step inserted
Earlier in the track, the cascade's tiebreakers were: origin, importance, specificity, source order. Layers slot in between origin and specificity. Simplified for author CSS:
- Step 1
1. Origin and importance
Author rules beat user-agent (browser default) rules; !important flips several of these comparisons.
- Step 2
2. Cascade layer order
Normal rules in a later-declared layer beat normal rules in an earlier-declared layer. Un-layered rules act as if they were in an implicit layer that comes after every named one. !important flips this: important rules in earlier layers beat important rules in later ones.
- Step 3
3. Specificity
Only used to decide between rules in the same layer. Specificity no longer competes across layers.
- Step 4
4. Source order
If everything above ties, the later rule wins.
The counter-intuitive piece — worth staring at for a second — is
that layer order is checked before specificity. A plain button
in a later layer beats a main .sidebar nav a.primary in an
earlier layer, regardless of specificity. That's the behaviour the
feature exists for.
Declaring the order once
The one line that does the real work is a bare @layer statement
listing the layers in the order you want:
@layer reset, base, components, utilities;That single line establishes four layers and fixes their relative order. Anything you later write inside one of them — in this file or any other — slots into the layer at that priority:
@layer reset {
* { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer base {
body { font-family: system-ui; line-height: 1.5; }
a { color: inherit; }
}
@layer components {
.button {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: var(--brand);
}
}
@layer utilities {
.sr-only {
position: absolute;
width: 1px; height: 1px;
overflow: hidden;
}
}A utility class in utilities will win over a component rule in
components, which wins over a base rule in base. Specificity
stops being the arbiter; the layer declaration is.
The implicit un-layered layer
CSS not written inside an @layer block doesn't vanish — it goes
into an implicit, unnamed group that ranks after every named
layer for normal declarations. In practice:
reset → base → components → utilities → (un-layered CSS)
This is the detail that makes incremental adoption realistic. You can move your reset and your design system into layers without touching the rest of the application's CSS, and the application CSS continues to behave exactly as it did before, because it still takes last word.
!important flips the order
Important rules follow the reverse layer order. In the layer
declaration above, an !important rule in reset would beat an
!important rule in utilities.
This sounds strange until you see what it's for: the browser's
user-agent styles are a layer that comes before every author
layer, and !important in the user layer (user stylesheets,
browser accessibility overrides) is specifically designed to
override author styles. The same logic applies to your own layers:
an "emergency override" rule you mark important in your reset
layer can override an important rule in your utilities, which is
the escape hatch you want when a specific component is behaving
badly.
Most well-structured code never needs this. It's useful to know
it's there, so an !important surprise in a reset layer isn't a
mystery.
Layering a vendor stylesheet at import
The practical trick that retires most of the "how do I override Bootstrap" category of problems:
@import url("https://cdn.example.com/vendor.css") layer(vendor);
@layer vendor, app;
@layer app {
.my-button {
background: var(--brand);
}
}Every rule in the vendor file is placed inside the vendor layer.
The layer declaration puts vendor before app. Anything you
write inside @layer app beats anything in vendor, regardless
of how specific the vendor selectors are. No !important
sprinkling, no .specific .more-specific .please selector
cosplay.
The same shape works for any third-party CSS you'd otherwise have to fight: a date picker, an admin UI kit, a markdown style sheet shipped by a library.
When not to reach for layers
Layers are infrastructure, not a tool for a single component. Two anti-patterns:
- Too many layers. Reset, base, components, utilities is a reasonable starting shape. 15 named layers for a team of three is a burden to reason about.
- Using a layer to hide a specificity problem inside one file. If one component's internal rules are competing, that's usually a sign that the component needs refactoring, not that it needs its own cascade layer.
The feature pays off at the level of "whole bodies of CSS whose relative priority I want to lock in." That's where to use it, and only there.
What to remember
- Cascade layers (
@layer) decide the order between groups of CSS rules before specificity is consulted. A plain selector in a later layer beats a specific selector in an earlier layer. - Declare the order once with a bare
@layer a, b, c;statement. Everything subsequently assigned to one of those layers takes its place. - Un-layered CSS acts as an implicit layer that comes after every named layer. This is the migration-friendly default.
!importantflips the order: important rules in earlier layers beat important rules in later ones. Useful for intentional escape hatches; surprising if you don't know it.@import url(...) layer(name)pins a third-party stylesheet into a layer, so your own code can override it without specificity wars.- Use layers to settle "which body of rules wins" at the architecture level. Don't reach for them to resolve a specificity problem inside a single component.
Check yourself
4 questions · pass 3/4 to unlock The Box Model
1.In the cascade order, how do cascade layers rank relative to specificity and
!important?2.Where does CSS that isn't inside any
@layerblock live in the cascade?3.What's the clearest reason to prefer
@layerover the long-standing 'just add another class selector' trick to boost specificity?4.A common pattern is to put a third-party stylesheet into its own layer so your app code can override it cleanly. What's the one-line way to do that at import time?
4 left to answer
Discussion
Sign in to postNo comments yet. Be the first to say something.