Lesson 11
Cascade Layers
CSS specificity was designed for small documents. A single stylesheet, a handful of selectors, and everything just worked. But modern projects have resets, base styles, component libraries, utility classes, and third-party CSS all competing for priority. The result? Specificity wars - developers stacking selectors, adding IDs, or reaching for !important just to make their styles win.
Cascade layers (@layer) solve this by letting you explicitly control which group of styles has priority - regardless of the specificity of individual selectors within those groups. Think of layers as a higher-level priority system that sits above specificity in the cascade.
Quick primer: how specificity is calculated
Before understanding layers, you need a solid grasp of specificity. Every selector has a specificity score expressed as three components: (A, B, C).
| Component | Counts | Example selectors |
|---|---|---|
| A - IDs | Number of ID selectors | #nav = (1,0,0) |
| B - Classes | Classes, attributes, pseudo-classes | .btn.primary = (0,2,0) |
| C - Elements | Type selectors, pseudo-elements | div p::before = (0,0,3) |
/* Specificity: (0, 1, 0) */
.button { background: blue; }
/* Specificity: (0, 2, 0) - wins over .button */
.sidebar .button { background: green; }
/* Specificity: (1, 0, 0) - wins over both */
#submit-btn { background: red; }
/* Specificity: (1, 1, 1) */
div#header .nav-link { ... }
/* The problem: to override .sidebar .button, you need
equal or higher specificity - leading to escalation */
.main-content .card .button { background: purple; } /* (0, 3, 0) */
The specificity problem: In large codebases, developers keep increasing specificity to override previous styles. Each override requires yet higher specificity, creating an arms race that makes CSS fragile and hard to maintain. Cascade layers break this cycle entirely.
The mental model: cascade resolution order
The cascade resolves conflicting declarations by checking these criteria in order. If a higher criterion produces a winner, lower criteria are never consulted:
- Transitions - active transition values always win
- !important (with layer inversion) - important declarations from earlier layers beat later ones
- Inline styles - the
styleattribute - Unlayered styles - CSS not inside any
@layer - Layers - later declared layers beat earlier ones
- Specificity - (A, B, C) comparison
- Source order - last rule in document wins
Notice that layers sit above specificity in this hierarchy. A selector with (0,1,0) in a higher-priority layer beats a selector with (1,0,0) in a lower-priority layer. This is the key insight - layer priority trumps specificity entirely.
Declaring and ordering layers
You declare layers with @layer. The order you declare them determines their priority - last declared has highest priority:
/* Declare layer order upfront (recommended) */
@layer reset, base, components, utilities;
/* Now add styles to each layer - order of these blocks doesn't matter */
@layer components {
.btn { background: blue; padding: 0.5rem 1rem; }
}
@layer utilities {
.bg-red { background: red; }
}
@layer base {
body { font-family: system-ui; line-height: 1.6; }
}
@layer reset {
*, *::before, *::after { box-sizing: border-box; margin: 0; }
}
In the declaration @layer reset, base, components, utilities, priority goes left to right - utilities has the highest priority because it's declared last.
Layer priority demo - the box gets its styles from the highest-priority layer
components layer wins over base because it's declared later.Declare your layer order at the top of your stylesheet. A single @layer reset, base, components, utilities; statement establishes the priority hierarchy. Then you can add styles to any layer from anywhere in your CSS, and the declared order (not the definition order) determines priority.
Declaration order vs definition order
This is the most critical distinction in cascade layers. There are two separate concepts:
- Declaration order: The order in which layer names are first introduced. This determines priority.
- Definition order: Where you actually write the styles for each layer. This does NOT affect priority.
/* Declaration order: A first, B second (B has higher priority) */
@layer A, B;
/* Definition order doesn't matter - even though A's styles
appear AFTER B's styles in the file, B still wins */
@layer B {
.box { background: blue; }
}
@layer A {
.box { background: red; }
}
/* Result: .box is blue (B wins because it was declared later) */
This is what makes layers powerful - you set the priority once at the top, and then you can organise your actual CSS in whatever file/import order makes sense for your project without worrying about accidentally changing priority.
Unlayered styles beat all layers
Any CSS that is not inside an @layer block is considered "unlayered" and has higher priority than all layered styles, regardless of specificity:
@layer utilities {
/* Even with !important specificity tricks... */
.special-box { background: gold; }
}
/* This unlayered style WINS over the layered one,
even though both target .special-box with (0,1,0) specificity */
.special-box { background: purple; }
Unlayered styles override layers - the purple background comes from unlayered CSS
base layer AND unlayered CSS. The unlayered purple background wins.Unlayered CSS is at the top of the layer hierarchy. This is by design - it provides an escape hatch. If you need to override layered styles quickly, write the override outside any layer. But in a well-structured project, most CSS should be in a layer, with unlayered styles used sparingly for one-off overrides.
A practical layer architecture
Here's a recommended layer structure for a typical project, from lowest to highest priority:
@layer reset, vendor, base, layout, components, utilities;
/* 1. reset - Normalize browser defaults */
@layer reset {
*, *::before, *::after { box-sizing: border-box; margin: 0; }
body { line-height: 1.5; -webkit-font-smoothing: antialiased; }
}
/* 2. vendor - Third-party libraries */
@layer vendor {
/* Imported via @import url("...") layer(vendor); */
}
/* 3. base - Typography, colours, design tokens */
@layer base {
:root { --accent: #2563eb; --text: #1a1a2e; }
body { font-family: system-ui, sans-serif; color: var(--text); }
a { color: var(--accent); }
}
/* 4. layout - Page-level structure */
@layer layout {
.container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
.grid { display: grid; gap: 1rem; }
}
/* 5. components - UI components */
@layer components {
.btn { padding: 0.5rem 1rem; border-radius: 4px; }
.card { border: 1px solid var(--border); border-radius: 8px; }
}
/* 6. utilities - Single-purpose overrides (highest priority) */
@layer utilities {
.hidden { display: none; }
.text-center { text-align: center; }
.mt-4 { margin-top: 1rem; }
}
This architecture mirrors how specificity should work logically: utilities override components, components override base styles, and base styles override resets. With layers, this works regardless of selector specificity - a utility class .hidden with specificity (0,1,0) reliably overrides a component selector .modal.active .panel with specificity (0,3,0).
!important and layers - the inversion
Here's where layers get surprising. For normal (non-important) declarations, later layers win. But for !important declarations, the priority inverts - earlier layers win:
| Priority (highest first) | Normal declarations | !important declarations |
|---|---|---|
| 1st | Unlayered | First-declared layer (e.g., reset) |
| 2nd | Last-declared layer (e.g., utilities) | Second-declared layer (e.g., base) |
| 3rd | ... | ... |
| Last | First-declared layer (e.g., reset) | Unlayered |
@layer reset, base, components, utilities;
@layer reset {
/* !important here has HIGHEST priority among layered !important */
img { display: block !important; }
}
@layer utilities {
/* Normal: utilities wins (last declared) */
.block { display: block; }
/* !important: utilities loses to reset's !important (first declared) */
.inline { display: inline !important; }
}
/* Why this makes sense: !important in a reset means "this must
NEVER be overridden" - and the inversion guarantees exactly that. */
Why the inversion exists: !important was always meant to be a "this must not change" signal. The inversion makes that intent reliable. A reset layer saying img { display: block !important; } truly cannot be overridden by later layers - because the first-declared layer has highest !important priority. This finally gives !important a sensible, predictable role.
Adding third-party CSS to a layer
You can import external stylesheets directly into a named layer using @import with the layer() function:
/* Import third-party CSS into a dedicated layer */
@import url("https://cdn.example.com/library.css") layer(vendor);
/* Or import into a new anonymous layer */
@import url("normalize.css") layer;
/* Layer order still determined by your declaration */
@layer reset, vendor, base, components, utilities;
/* Now the third-party CSS has lower priority than your components
and utilities - no more fighting their selectors! */
This is transformative for managing third-party CSS. Instead of fighting specificity battles with Bootstrap, Tailwind preflight, or any other library, you simply put it in a layer below your own code. Your styles always win without needing to increase specificity.
/* Real-world example: taming a UI library */
@layer reset, vendor, theme, components, utilities;
@import url("open-props/style.css") layer(vendor);
@import url("some-component-lib/styles.css") layer(vendor);
@layer theme {
/* Your design tokens override the library's defaults
without needing higher specificity */
:root {
--brand: #2563eb;
--radius: 8px;
}
}
@layer components {
/* Your component styles always beat the vendor layer,
even if the library uses .namespace .component .element */
.btn {
background: var(--brand);
border-radius: var(--radius);
}
}
Nested layers
Layers can be nested inside other layers using dot notation or block nesting. Nested layers are scoped to their parent:
/* Dot notation for declaring nested layers */
@layer components.buttons, components.cards, components.forms;
/* Block nesting syntax */
@layer components {
@layer buttons {
.btn { padding: 0.5rem 1rem; }
}
@layer cards {
.card { border-radius: 8px; }
}
}
/* Equivalent dot notation for adding styles */
@layer components.buttons {
.btn-primary { background: blue; }
}
/* Priority: within the components layer,
forms > cards > buttons (last declared wins) */
Nested layers are useful for organising large layers. A components layer might contain sub-layers for different component categories. The parent layer's position in the top-level hierarchy is unchanged - nesting only affects priority within that parent.
Anonymous layers
You can create layers without naming them. Anonymous layers are useful when you want to isolate styles at a lower priority without tracking a name:
/* Anonymous layer - cannot be referenced later */
@layer {
.legacy-widget { color: grey; font-size: 14px; }
}
/* Another anonymous layer - each is separate */
@layer {
.old-component { margin: 10px; }
}
/* Anonymous layers are ordered by their position in the source.
You can't add more styles to them later because they have no name.
Useful for one-shot isolation of legacy CSS. */
When to use anonymous layers: They're ideal for wrapping legacy CSS you don't control and don't need to extend. Since they can't be referenced by name, they act as sealed containers. Named layers are better for anything you'll actively maintain or add to over time.
How this changes your workflow
Here's a before/after comparison of how you handle specificity conflicts:
| Scenario | Before (no layers) | After (with layers) |
|---|---|---|
| Utility class not applying | Add !important or increase specificity |
Utilities layer already wins - just works |
| Third-party styles overriding yours | Match or exceed their specificity | Put vendor CSS in a lower layer |
| Reset styles leaking into components | More specific selectors on components | Reset in lowest layer, components always win |
| File/import order changing priority | Careful ordering of stylesheets | Declaration order is explicit and separate from file order |
| Collaborator writes high-specificity CSS | Cascade breakage, debugging pain | Their layer determines priority, not their selectors |
The fundamental shift: instead of managing specificity at the selector level (which is fragile and doesn't scale), you manage priority at the architectural level. Each category of CSS has a defined priority, and individual selectors within that category compete only with each other.
Browser support
Cascade layers (@layer) are supported in all modern browsers: Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+. That covers over 95% of global users as of 2024. For older browsers, unlayered CSS is simply treated normally - it doesn't break, it just doesn't get the layer prioritisation benefits.
/* Feature detection if needed */
@supports at-rule(@layer) {
/* Layer-aware styles */
}
/* In practice, you rarely need this.
Browsers that don't support @layer simply ignore the layer
declarations and treat all styles as unlayered (normal behaviour). */
Retrieval check
Question 1
Where do layers sit relative to specificity in the cascade resolution order?
Question 2
Given @layer reset, base, components, utilities - which layer has the highest normal priority?
Question 3
What beats all layered styles in the cascade?
Question 4
For !important rules, how is layer priority resolved?
Question 5
How do you put third-party CSS into a named layer?