@layer — The CSS Feature That Finally Kills Specificity Wars
You've been fighting specificity for years. !important everywhere, selectors getting longer and longer. @layer fixes the root cause — and Tailwind v4 already uses it.
I was reviewing a codebase last month. The stylesheet had 47 instances of !important.
Every single one existed for the same reason: a developer couldn't figure out why their style wasn't winning, so they forced it.
That's not a developer problem. That's a CSS architecture problem. And @layer is the fix the language was missing for 25 years.
Why Specificity Becomes a War
CSS decides which rule wins using three things, checked in order:
The problem: you had no control over #1. Origin was fixed. So every conflict became a specificity fight.
What @layer Actually Does
@layer introduces cascade layers — a new slot in the cascade that sits between "where did this come from" and "how specific is the selector." Layer order is checked before specificity.
The full cascade order for author styles (highest → lowest priority, normal rules):
A a { } selector in utilities beats #app.container nav a { } in base. Layer order wins. Specificity is irrelevant between layers.
The Three Ways to Use It
1. Declare order upfront — do this first, always
/* Top of your stylesheet — sets the entire priority order */
@layer reset, tokens, base, components, utilities;
The most important pattern. You declare the order once. Then define rules anywhere — the declared order is what matters, not where the @layer blocks appear in the file.
2. Block — declare and define together
@layer base {
p { color: gray; line-height: 1.6; }
}
@layer utilities {
.text-red { color: red; } /* always beats base, regardless of specificity */
}
3. Anonymous layer
@layer {
p { margin-block: 1rem; }
}
Can't add to it later. Priority is determined by where it appears in the file. Useful for one-off third-party chunks you don't want to name.
The Rule That Surprises Everyone
Styles outside any layer always beat styles inside a layer.
@layer utilities {
.btn { color: red; } /* loses */
}
.btn { color: blue; } /* wins — not in any layer */
This is intentional — existing CSS you haven't migrated yet won't suddenly break. But it has a critical implication:
Third-party CSS loaded without a layer will override everything in your layers. Bootstrap, any UI library, any CDN stylesheet — if it's not wrapped in a layer, it beats your entire layer system.
/* ❌ Bootstrap is unlayered — overrides your entire @layer system */
@import "bootstrap.css";
/* ✅ Wrap it — now your layers win */
@import "bootstrap.css" layer(bootstrap);
@layer bootstrap, base, components, utilities;
Note: @import must come before any other rules except @charset and @layer statement declarations (not blocks). This is a hard browser requirement -- violating it silently ignores the import.
Specificity Still Matters — Inside the Same Layer
Layer order replaces specificity between layers. Inside a single layer, specificity works exactly as before.
@layer utilities {
a { color: red; } /* specificity: 0,0,1 */
.link { color: blue; } /* specificity: 0,1,0 — wins within this layer */
#nav a { color: green; } /* specificity: 1,0,1 — wins within this layer */
}
The rule: layer order first → then specificity → then source order. Specificity only matters when two rules are in the same layer.
!important Flips the Entire Order
Normal rules: last declared layer wins. !important rules: first declared layer wins.
@layer base, utilities;
@layer base {
p { color: black !important; } /* wins — first layer + !important */
}
@layer utilities {
p { color: red !important; } /* loses — even though utilities is last */
}
Practical advice: avoid !important inside layers entirely. if you need to override something put it in a later layer. That's the whole point of @layer.
Recommended by LinkedIn
The revert-layer Keyword — The Power Feature Nobody Talks About
revert-layer rolls a property back to what it would have been in the previous layer. It's the escape hatch for when a component sets something you want to undo in a specific case — without knowing or repeating the original value.
@layer base, components, utilities;
@layer components {
.card { background: white; padding: 1rem; border-radius: 8px; }
}
@layer utilities {
/* Don't override background — roll it back to whatever base had */
.card-transparent { background: revert-layer; }
Without revert-layer, you'd have to know what base set and repeat it. With it, you just say "use whatever the layer below me had." Clean, maintainable, no magic values.
Nesting Layers
@layer framework {
@layer reset { * { box-sizing: border-box; } }
@layer layout { .grid { display: grid; } }
@layer theme { :root { --color: #0073b1; } }
}
/* Add to a nested layer later using dot notation */
@layer framework.layout {
.flex { display: flex; gap: 1rem; }
}
Inside framework, priority is: reset < layout < theme. The entire framework layer sits wherever you declared it in the outer order.
Real-World Patterns
Design system — the complete setup
@layer reset, tokens, base, components, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
img { display: block; max-width: 100%; }
}
@layer tokens {
:root {
--color-primary: #0073b1;
--color-text: #1e293b;
--space-4: 1rem;
--radius: 6px;
}
}
@layer base {
body { font-family: system-ui, sans-serif; color: var(--color-text); line-height: 1.5; }
a { color: var(--color-primary); }
}
@layer components {
.btn {
padding: 0.5rem 1rem;
background: var(--color-primary);
color: white;
border-radius: var(--radius);
}
.card { background: white; border-radius: var(--radius); padding: var(--space-4); }
}
@layer utilities {
.mt-4 { margin-top: var(--space-4); }
.text-center { text-align: center; }
.hidden { display: none; }
}
Incremental adoption in a legacy codebase
/* Step 1: Wrap ALL existing CSS in a low-priority layer */
@layer legacy {
/* ...all your existing CSS... */
}
/* Step 2: New CSS outside layers — beats legacy automatically */
.new-component { color: blue; }
/* Step 3: Over time, migrate into proper named layers */
@layer legacy, base, components, utilities;
The migration isn't all-or-nothing. A practical approach:
Performance note: @layer has zero runtime cost. Browsers resolve layer order at parse time — it's a cascade rule, not a runtime operation.
Tailwind v4 Uses @layer Natively
Tailwind CSS v4 (2025) is built entirely on @layer:
/* Tailwind v4 generates this automatically */
@layer theme { /* design tokens */ }
@layer base { /* element resets */ }
@layer components { /* component classes */ }
@layer utilities { /* utility classes — always win */ }
If you use Tailwind v4, you're already using @layer. Understanding it explains why bg-red-500 always overrides your component's background — it's in a later layer, not because of specificity magic.
Questions Experienced Developers Ask
CSS-in-JS heads-up: styled-components, Emotion, and most CSS-in-JS libraries inject styles as unlayered <style> tags — which beat your entire @layer system. If your project uses CSS-in-JS heavily, read the Q&A below before adopting @layer
Does this work with CSS-in-JS (styled-components, Emotion)?
Partially. These libraries inject styles via <style> tags as unlayered styles — which beat your layers. This is a known limitation. Most CSS-in-JS libraries haven't adopted @layer in their injection mechanism yet.
What about <style> blocks in Vue/Svelte components?
Same issue — SFC <style> blocks are injected as unlayered styles. The fix: use @layer inside the component's <style> block too.
<style>
@layer components {
.my-component { color: blue; }
}
</style>
Does re-declaring a layer name cause problems?
No — it appends rules to the existing layer. The layer order is set by the first declaration. Re-declaring just adds more rules to it. This is how you split a layer across multiple files.
What about @layer inside media queries?
Supported. The layer is still registered globally — the media query only controls when the rules inside apply, not the layer's position in the order.
Can I use @layer with Shadow DOM?
Layers are scoped to their stylesheet. A @layer inside a Shadow DOM stylesheet doesn't interact with layers in the main document. Each shadow root has its own cascade context.
Quick Reference
Browser Support
Baseline Widely Available. No polyfill needed. Safe in production today.
Browser compatibility data from MDN Web Docs — @layer.
The specificity war was never a developer skill problem. It was a missing language feature. You couldn't declare intent — you could only engineer around the absence of it.
@layer gives you that intent. One line at the top of your stylesheet, and the entire priority system is explicit, readable, and under your control.