Specificity has been CSS's most misunderstood and frequently fought-against feature for three
decades. @layer doesn't remove that fight — it gives you the referee.
If you've ever added !important to a rule just to beat a third-party stylesheet,
written increasingly absurd selectors like #app .sidebar ul li a.active, or
discovered that the order of your CSS imports matters in ways you didn't intend — you've been
losing a war that CSS Cascade Layers were designed to end.
Introduced in the CSS Cascading and Inheritance Level 5 specification and now supported in
all modern browsers, @layer is the biggest change to how CSS resolves conflicts
since the cascade itself was invented. This article explains what it is, why it exists,
and how to use it to write CSS you'll actually enjoy maintaining.
The Specificity Problem, Honestly Explained
The CSS cascade resolves style conflicts by checking, in order: origin & importance, then specificity, then source order. In theory this is elegant. In practice, specificity turns into an arms race.
Consider this common scenario: you're using a CSS framework (Bootstrap, Tailwind base styles, a design system). You want to override a button color. You write:
/* Your override */
.btn-primary {
background-color: hotpink;
}
But the framework uses .btn.btn-primary — two classes, higher specificity — and
your rule never applies. You add your own extra class. The framework updates and adds an ID.
You reach for !important. You've lost the plot.
The root cause: CSS has no built-in concept of provenance. It doesn't know that
your utility class should always beat the framework's reset, or that your component styles
should override your base styles — regardless of specificity. @layer solves
exactly this.
What @layer Actually Does
A cascade layer is an explicitly named bucket for your CSS. When a conflict arises between two rules, layer order beats specificity. Rules in a later layer win over rules in an earlier layer, no matter how many classes or IDs the earlier rule has.
The basic syntax is simple:
@layer reset, base, components, utilities;
@layer reset {
* { box-sizing: border-box; margin: 0; }
}
@layer base {
h1 { font-size: 2rem; }
}
@layer components {
/* Even a low-specificity rule here beats base */
.card h1 { font-size: 1.5rem; }
}
@layer utilities {
.text-sm { font-size: 0.875rem; }
}
That first line — @layer reset, base, components, utilities; — is the
declaration of priority. Layers listed later have higher priority. A single
class in utilities will always beat a three-class selector in base.
You only need to declare the layer order once, typically at the top of your main CSS file. After that, you can write rules into those layers anywhere — even across multiple files — and the priority is always determined by that single declaration.
Layer Priority (Lowest → Highest)
Unlayered Styles: The Silent Top Layer
Here's a subtlety that trips everyone up at first: styles written outside any
@layer block automatically have higher specificity than any layered
style. They sit above all layers in the cascade.
@layer utilities {
.text-red { color: red; }
}
/* This unlayered rule beats .text-red even though
it has lower specificity — it's outside all layers */
p { color: navy; }
This matters enormously when adopting @layer in an existing codebase, or when
wrapping third-party CSS. The golden rule: if you want a stylesheet to be
overrideable, wrap it in a layer.
When you import a third-party library, any styles it declares outside a layer will be treated as unlayered and will beat your own layered styles. Always wrap third-party imports in a named layer to contain them.
Practical Patterns for Real Codebases
1 — Containing a third-party library
This is the killer use case. Wrapping an external stylesheet in a layer means it can never beat your own styles, regardless of what selectors it uses:
/* Bootstrap is now fully contained in the lowest layer */
@import url("bootstrap.min.css") layer(framework);
@layer framework, base, components, utilities;
/* This single class now reliably overrides any Bootstrap rule */
@layer components {
.btn-primary { background-color: hotpink; }
}
Notice the layer() function in the @import statement — this is the
cleanest way to layer an entire external stylesheet in one line.
2 — Design system architecture
Layers map naturally onto the structure of a design system. A typical layering order might be:
@layer
reset, /* Normalize / modern-normalize */
tokens, /* CSS custom properties */
base, /* Element defaults (h1-h6, p, a…) */
layout, /* Grid, flex wrappers */
components, /* Button, Card, Modal… */
patterns, /* Composed component combinations */
utilities, /* Single-property helpers */
overrides; /* Emergency escape hatch (rare) */
3 — Sub-layers for component variants
Layers can be nested, which is useful for grouping component states and variants without introducing specificity complexity:
@layer components {
@layer base, variants, states;
@layer base {
.btn {
padding: 0.5em 1.2em;
border-radius: 4px;
}
}
@layer variants {
.btn-ghost { background: transparent; }
.btn-solid { background: var(--color-primary); }
}
@layer states {
.btn:disabled { opacity: 0.4; }
}
}
The full sub-layer name for reference purposes would be
components.base, components.variants, etc.
The !important Reversal
This one will bend your brain: !important reverses layer
order. A rule marked !important in a lower-priority layer beats
!important in a higher-priority layer.
@layer base, utilities;
@layer base {
/* !important in a lower layer beats !important in a higher layer */
.text { color: red !important; }
}
@layer utilities {
/* This !important loses despite utilities being higher priority */
.text { color: blue !important; }
}
/* Result: red */
This is intentional and mirrors how !important works across cascade origins
(user stylesheets using !important beat author stylesheets using
!important). The practical takeaway: avoid !important
in layered CSS entirely. You don't need it anymore — that's the whole point.
Think of !important as inverting the cascade for a specific rule.
In a layers world, you should be able to manage priority through layer order alone.
If you find yourself reaching for !important within layered CSS,
it's usually a sign the layer order itself needs redesigning.
@layer vs. Other Approaches
| Approach | How it manages conflicts | Drawback |
|---|---|---|
| High specificity selectors | Win by having more IDs/classes | Arms race; unmaintainable over time |
| !important | Brute-force override | Makes future overriding nearly impossible |
| CSS Modules / Shadow DOM | Scoped selectors eliminate global conflicts | Requires build tooling; cross-component theming is complex |
| BEM naming | Convention-based uniqueness | Discipline-dependent; still loses to specificity from other systems |
| @layer | Explicit cascade priority buckets | Requires intentional architecture up front |
@layer isn't a replacement for CSS Modules or scoping strategies — they solve
different problems. Scoping prevents global collisions; layers manage priority
between systems that coexist in the global scope.
Browser Support and Progressive Adoption
As of 2026, @layer has full support in all modern browsers — Chrome 99+,
Firefox 97+, Safari 15.4+, and Edge 99+. Global browser support is above 95%.
For the small percentage of users on older browsers, unlayered fallback is straightforward:
browsers that don't understand @layer will simply ignore the layer wrapper and
apply the rules in source order, which usually gives a reasonable result.
You can also adopt @layer incrementally. Start by wrapping just your third-party
dependencies. Then, as you refactor files, move them into layers. There's no requirement
to do it all at once.
Start small: wrap a single third-party CSS import in a layer and add a top-level layer declaration. You'll immediately gain control over that library without touching any other code. Then layer your own code incrementally as you touch each file.
The Future: What @layer Enables
@layer is more than a bug fix for specificity. It's an enabling primitive
that changes what's practical in CSS architecture:
Design token layers. Put all your custom properties in a
tokens layer. Override them per-theme or per-context in later layers.
Theming becomes predictable.
Safe library interop. Use multiple CSS frameworks simultaneously — something previously almost impossible — by giving each its own layer with a clear priority relationship.
Utility-first without the footgun. Utility classes are famously hard to
override when you need to. Placing them in the highest layer means they always win when
applied — but because they're in a named layer, you can still override them
in an overrides layer you explicitly declare higher.
Tooling and frameworks. Build tools can now generate CSS that respects a known layer architecture. The next generation of CSS frameworks — several are already experimenting with this — will ship pre-layered, allowing drop-in use without specificity fighting.
Stop Fighting the Cascade. Own It.
The specificity wars were never really about CSS being broken. They were about the cascade lacking a tool for expressing intent — your intent that utility classes should beat component defaults, that your code should beat third-party code, that resets should lose to everything else.
@layer gives CSS that missing vocabulary. It doesn't make specificity
irrelevant — specificity still governs conflicts within a layer — but it makes
the most common source of pain, conflicts between systems, entirely tractable.
Start with one layer. Name it after where the styles come from. Add a second layer above it.
You'll quickly discover you've written your last !important.