I used to fight CSS more than I care to admit.
Not because I didn't understand it. But because at scale, the cascade stops feeling like a system and starts feeling like luck. You tweak a selector, add a class, move a file — something breaks.
@layer is the first feature that made the cascade feel intentional again.
Before @layer, you really only had three levers:
-
specificity
-
source order
-
!important (the nuclear option)
So we built habits like long selectors, strict file ordering, or utility classes with !important. These work, but they are indirect. You're shaping behavior by side effects.
@layer changes that. It lets you say:
"This group of styles should always win over that group.""
That's it. Direct control.
The easiest way to think about layers is as priority buckets.
@layer reset, base, components, utilities;Now you've defined a stack:
reset < base < components < utilities
Anything in utilities beats anything in components. It doesn't matter how specific the selector is.
That's the key shift.
Here's a simple example:
@layer components {
.card {
padding: 8px;
}
}
@layer utilities {
.p-4 {
padding: 16px;
}
}<div class="card p-4"></div>You get:
padding: 16px;Even though .card might feel more "important". Layer order wins.
This starts to shine in real systems.
If you mix components and utilities, things get messy fast without rules. You want components to define structure, and utilities to tweak things.
@layer components {
.btn {
padding: 8px 12px;
border-radius: 6px;
}
}
@layer utilities {
.p-0 {
padding: 0;
}
}Now utilities always win. No hacks, no !important.
It also works well for design systems.
You can map layers to how your team already thinks:
@layer reset, tokens, base, components, overrides;-
reset: normalize styles
-
tokens: variables and design tokens
-
base: typography and globals
-
components: UI pieces
-
overrides: edge cases
This structure holds up even as the codebase grows.
One of the most useful parts is handling third-party CSS.
@import url('normalize.css') layer(reset);
@import url('some-library.css') layer(components);Now your app styles can always override those libraries in a clean way. No guessing selector weight. No digging into their source.
There's one rule that trips people early:
Unlayered styles beat all layered styles.
.card {
padding: 32px;
}This overrides everything inside @layer.
It's intentional, but easy to miss.
The fix is simple: pick a strategy. Either put everything in layers, or treat unlayered CSS as top priority. Don't mix casually.
A few pitfalls that come from real use:
Not declaring layer order upfront.
@layer components {
...
}
@layer utilities {
...
}This looks fine, but now order depends on load order. That's fragile.
Instead, declare it once:
@layer base, components, utilities;Do this early and forget about it.
Assuming specificity still matters across layers.
It doesn't.
@layer components {
.card .title {
color: blue;
}
}
@layer utilities {
.text-red {
color: red;
}
}<h1 class="card title text-red"></h1>This will be red. Every time.
Layer beats specificity.
Overusing layers.
Not every project needs this. If your app is small or already consistent, layers can feel like extra structure with no gain.
They help most when, multiple style approaches collide ,teams work in parallel, you import external CSS, or you want to enforce a clear separation of concerns.
Debugging feels a bit different too.
When something doesn't apply, you now check:
-
layer order
-
specificity
-
source order
It's one more axis, but it's also more predictable once you get used to it.
The structure I keep coming back to is simple:
@layer reset, base, components, utilities, overrides;-
reset: normalize, box-sizing
-
base: body, typography
-
components: UI pieces
-
utilities: spacing, colors
-
overrides: rare fixes
Rules stay boring:
everything goes into a layer utilities come last overrides stay rare
This alone removes most cascade surprises.
There's also a subtle benefit. You stop writing defensive CSS.
No more stacking classes just to win:
.card.card-large.special {
...
}You just place styles in the right layer.
Where this gets interesting is where CSS might go next.
This feels like a shift toward more architectural CSS. Less naming tricks, fewer conventions, more intent built into the language itself.
Instead of asking “how do I win the cascade?”, you ask “where does this belong?”
That's a better question.
@layer doesn't replace the cascade. It makes it predictable.
You get fewer specificity hacks, fewer !importants, and fewer surprises.