Chapter 1 — @layer and Modern Cascade Control
Cascade layers are the most significant addition to the CSS cascade since specificity
was defined. They add an explicit priority tier above specificity — one that
you control by declaration order rather than by selector weight. This chapter goes
beyond the introduction in the Overview course and covers the full algorithm, every
edge case, the revert-layer keyword, anonymous layers, nested layer
hierarchies, @import layer() for vendor sandboxing, and how to debug
layers in Chrome and Firefox DevTools.
Prerequisite. This chapter assumes familiarity with @layer basics
from the Overview course (Ch 10–11). Here we go to the algorithm level — reading
browser source-style precision into every rule.
1. The Full Cascade Algorithm
The cascade is not a single check — it is a sequence of five filters applied in
order. The browser stops at the first filter that produces a single winner. Only if a
filter results in a tie does it proceed to the next one. Understanding the exact order
is what makes cascade layers predictable.
1
Origin + importance
User-agent < author < user stylesheet. !important reverses this order completely.
Always first
2
Cascade layer
Within the same origin, the layer declared last wins. Unlayered styles beat all layers.
@layer — new
3
@scope proximity
The @scope root closest to the element wins when two scopes compete.
@scope — new
4
Specificity
A-B-C scoring: IDs > classes/attributes/pseudos > elements. Only reached after layers and scope tie.
Classic
5
Source order
Last declaration wins when all previous filters tie. The original tiebreaker.
Classic
The critical insight: cascade layer (step 2) is resolved before specificity
(step 4). A zero-specificity utility class in a higher-priority layer
permanently beats a 3-ID chain in a lower-priority layer. Specificity is irrelevant
across layer boundaries — it only matters within the same layer.
/* Proof: layer beats specificity across boundaries */
@layer base, components, utils;
@layer components {
/* Very high specificity: 0-3-0 */
.sidebar .card .card__title { color: #58a6ff; }
}
@layer utils {
/* Low specificity: 0-1-0 — but utils is higher priority than components */
.text-red { color: #ff7b72; }
}
/* <h2 class="card__title text-red"> → red wins */
/* No !important, no higher specificity needed — layer order decides it */
2. Layer Ordering — Every Rule
The ordering statement
/* A @layer statement with no block declares the order */
@layer vendor, base, components, utils;
/* "utils" wins any conflict — it's declared last */
/* Layers can be filled in any order after the statement */
@layer utils { .mt-4 { margin-top: 1rem; } }
@layer vendor { .btn { padding: 8px; } } /* still lowest priority */
@layer components { .btn { padding: 10px; } }
/* Order is set by the STATEMENT, not by where the @layer blocks appear */
/* First-seen rule — if there is no statement, order is determined by */
/* the first time each layer name appears in the stylesheet */
@layer A { .x { color: red; } } /* A is first-seen → lowest */
@layer B { .x { color: blue; } } /* B is second → higher → blue wins */
/* Avoid relying on first-seen order — always use an explicit statement */
Unlayered styles always win
1
unlayered
Any style NOT inside an @layer block. Always wins regardless of specificity.
0-1-0
2
@layer utils
Declared last in the order statement — highest priority among layers.
0-3-0
3
@layer components
Middle priority.
0-2-0
4
@layer base
Low priority — even with high specificity, loses to higher layers.
0-3-0
5
@layer vendor
Declared first — lowest priority of all. Third-party CSS sandboxed here.
1-0-0
Unlayered styles as a safe default. Because unlayered styles beat
all layers, any existing CSS that you haven't migrated to a layer automatically
takes priority. This makes incremental adoption safe — you can layer new code
while old code continues to work. But it also means you must be deliberate:
accidental unlayered styles will silently override everything inside your layers.
/* Gotcha: unlayered styles in an imported file */
@import url('legacy.css'); /* NOT layered — beats all your @layers */
@import url('legacy.css') layer(vendor); /* sandboxed — safe */
/* Wrapping an existing stylesheet in a layer after the fact */
@layer vendor {
@import url('bootstrap.css'); /* invalid — @import inside @layer block */
}
/* Correct syntax: @import url() layer() — the layer() goes on the import */
@import url('bootstrap.css') layer(vendor); /* correct */
3. !important — The Reversal
Inside !important declarations the layer priority order is completely
reversed. This mirrors how the origin stack works (user-agent !important beats author
!important) and exists for the same reason — it lets lower-priority layers enforce
overrides that cannot be beaten by higher-priority ones.
Normal declarations — last layer wins
!important declarations — REVERSED
/* Practical consequence: !important in vendor layer beats your !important */
@layer vendor, base, components, utils;
@layer vendor {
.btn { display: inline-block !important; }
}
@layer utils {
.hidden { display: none !important; }
/* utils is highest priority for NORMAL declarations */
/* but for !important, vendor is highest — vendor wins here */
}
/* This is why you should NEVER put !important inside @layer blocks. */
/* It creates a hidden inverse priority system that is very hard to debug. */
/* @layer exists specifically so you don't need !important at all. */
The rule. Never use !important inside a layer.
If you need a declaration to always win, put it in unlayered styles or in the
highest-priority layer as a normal declaration. If a third-party stylesheet uses
!important, wrap it in layer(vendor) — that
!important then becomes the lowest-priority important and
your own normal declarations will beat it.
4. Anonymous Layers
/* An @layer block with no name creates an anonymous layer */
@layer {
.tooltip { position: absolute; }
}
/* Anonymous layers: */
/* - Cannot be referenced again — each anonymous block is its own layer */
/* - Cannot be reordered via a statement — they take priority by source order */
/* - Useful for one-off styles you never want overridden by named layers */
/* - Useful for quick sandboxing without naming the layer */
@layer named; /* low priority */
@layer { .a { color: red; } } /* anonymous-1: position sets priority */
@layer named { .a { color: blue; } } /* named layer — declared before anonymous-1 → lower priority */
@layer { .a { color: green; } } /* anonymous-2: appears last → highest of all layers */
/* Result: green wins — anonymous-2 is last, so highest priority */
/* Practical use: quick vendor wrap without inventing a name */
@import url('plugin.css') layer(); /* anonymous layer via @import */
5. Nested Layers
Layers can be nested inside other layers, creating a sub-priority hierarchy within
a parent layer. Nested layers are only visible within their parent — they cannot affect
the priority of anything outside it.
@layer components
@layer base, variants, states;
@layer base { .btn { ... } } lowest within components
@layer variants { .btn--ghost { ... } }
@layer states { .btn:hover { ... } } highest within components
@layer utils
still beats ALL of components, regardless of component nesting
/* Nested layer syntax */
@layer components {
/* Declare sub-layer order first */
@layer base, variants, states;
@layer base {
.btn {
display: inline-flex;
padding: 0.5rem 1rem;
border-radius: 6px;
background: var(--color-primary);
transition: transform 0.15s ease;
}
}
@layer variants {
.btn--ghost {
background: transparent;
border: 1px solid currentColor;
}
}
@layer states {
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: scale(0.97); }
.btn:disabled{ opacity: 0.4; pointer-events: none; }
}
}
/* Reference nested layers with dot notation */
/* components.base, components.variants, components.states */
/* Useful when debugging in DevTools — full dotted path shown */
/* Appending to a nested layer from outside — works, but consider readability */
@layer components.states {
.btn.is-loading { cursor: wait; }
}
6. The revert-layer Keyword
revert-layer is a CSS-wide keyword (like initial or
inherit) that rolls a property back to the value it would have had in
the next lower-priority layer. It is the @layer-aware equivalent
of revert, which rolls back to the browser stylesheet.
/* revert vs revert-layer */
@layer base { .link { color: #58a6ff; } }
@layer components { .nav .link { color: #fff; } }
@layer utils {
.reset-color {
color: revert-layer;
/* rolls back to what components layer says: #fff */
/* NOT back to base, NOT back to initial */
}
}
/* revert-layer on an element in the components layer */
@layer components {
.card--plain .card__title {
font-size: revert-layer;
/* rolls back to the value from the NEXT LOWER layer (base) */
/* if base has no value, rolls back to the browser stylesheet */
/* if no browser stylesheet value, falls back to initial */
}
}
/* Classic use case: opt a specific element OUT of a component layer rule */
@layer base, components;
@layer base { button { padding: 0.5rem 1rem; } }
@layer components {
button { padding: 0.75rem 1.5rem; }
button.compact { padding: revert-layer; }
/* .compact button gets the base layer padding: 0.5rem 1rem */
}
7. @import + layer() — Vendor Sandboxing
/* Basic syntax */
@import url('normalize.css') layer(base);
@import url('bootstrap.css') layer(vendor);
@import url('animation-lib.css') layer(); /* anonymous layer */
/* Combine with media/supports conditions */
@import url('print.css')
layer(print)
supports(display: grid)
print;
/* The layer() must appear BEFORE supports() and media conditions */
/* Order: url() → layer() → supports() → media */
/* What sandboxing achieves: */
/* Before: Bootstrap's .container has high specificity → hard to override */
/* After: Bootstrap is in vendor layer → a zero-specificity utility beats it */
@layer vendor, base, components, utils;
@import url('bootstrap.css') layer(vendor);
@layer utils {
/* This beats Bootstrap's .container-fluid.px-4 (however specific) */
.p-0 { padding: 0; }
}
/* @import must be at the top of the file — before any rule blocks */
/* The layer order statement must also come before the @imports that fill layers */
/* Correct file structure: */
/* 1. @layer order statement */
/* 2. @import url() layer() statements */
/* 3. @layer blocks with your own rules */
8. Layers with @media, @supports, and @scope
/* @layer inside @media — valid and useful */
@media (min-width: 768px) {
@layer layout {
.sidebar { display: block; }
}
}
/* This appends to the 'layout' layer, but only on screens >= 768px */
/* Layer priority is unchanged — it's still in the 'layout' layer */
/* @layer inside @supports — feature-gate whole layer blocks */
@supports (container-type: inline-size) {
@layer components {
.card { container-type: inline-size; }
}
}
/* @scope inside @layer — scope IS layer-aware */
@layer components {
@scope (.card) {
.title { font-size: 1.25rem; }
}
}
/* Scope proximity resolves WITHIN the same layer */
/* Layer priority still applies first */
/* CSS nesting and @layer — fully compatible */
.card {
background: var(--surface);
/* @layer nested inside a rule — adds to that layer globally */
@layer state {
&.is-selected { outline: 2px solid var(--accent); }
}
}
/* Equivalent to: @layer state { .card.is-selected { outline: ... } } */
9. Debugging Layers in DevTools
/* Inspecting layer structure from JavaScript */
const sheets = [...document.styleSheets];
for (const sheet of sheets) {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSLayerBlockRule) {
console.log('Layer block:', rule.name); // 'components'
}
if (rule instanceof CSSLayerStatementRule) {
console.log('Layer order:', rule.nameList); // ['vendor', 'base', ...]
}
}
}
10. Real-World Layer Strategy Patterns
/* ── Pattern A: Design system with consumer overrides ─────────── */
/* The design system ships with lower-priority layers so consumers can override */
/* design-system/index.css (consumed as a package) */
@layer ds.reset, ds.tokens, ds.base, ds.components;
@layer ds.components {
.button { background: var(--ds-color-primary); }
}
/* consumer/app.css */
@layer ds.reset, ds.tokens, ds.base, ds.components, overrides;
@import url('design-system/index.css') layer();
/* anonymous layer wraps the entire design system at low priority */
/* consumer's own rules outside any layer beat all of it */
/* ── Pattern B: Multi-team monorepo ─────────────────────────────── */
/* Each team owns a named sub-layer, ordered by the platform team */
@layer platform, team-a, team-b, team-c, overrides;
@import url('platform/styles.css') layer(platform);
@import url('team-a/styles.css') layer(team-a);
@import url('team-b/styles.css') layer(team-b);
/* Conflicts between teams are resolved by the priority order */
/* The platform team can set the order and each team's work stays sandboxed */
/* ── Pattern C: Progressive enhancement ──────────────────────────── */
/* Older browsers don't support @layer — ensure they still work */
@supports not (selector(@layer test { })) {
/* Styles for browsers without @layer support */
.btn { background: blue; }
}
/* In practice: @layer is Baseline 2022 — all evergreen browsers support it */
/* Only IE and very old Edge (pre-Chromium) do not. For those, unlayered fallback. */
Chapter Summary
| Concept | Key point |
| Cascade position | Layer is step 2 of 5 in the cascade — resolved before specificity. A layer win cannot be overridden by specificity in a lower-priority layer. |
| Order rule | The layer declared last in the order statement wins. Unlayered styles always beat every named layer, regardless of specificity. |
| Order statement | Always declare order with @layer a, b, c; at the top of main.css. Never rely on first-seen order across multiple files. |
| !important reversal | Inside !important, layer priority reverses — the lowest-priority layer's !important beats the highest-priority layer's !important. Never use !important inside a layer. |
| Anonymous layers | @layer { } with no name creates a one-off anonymous layer ordered by source position. Cannot be appended to. Useful for quick vendor wrapping. |
| Nested layers | Layers can nest — the dotted path is components.states. Nesting creates a sub-hierarchy within the parent layer. External layers still beat the entire parent. |
| revert-layer | Rolls a property back to the value from the next lower layer. Used to opt one element out of a rule without !important or specificity escalation. |
| @import layer() | @import url() layer(name) assigns the imported stylesheet to a named layer. Must appear before any rule blocks. @import inside a @layer block is invalid. |
| DevTools | Chrome shows layer badges in the Styles panel and a Layers pane for priority overview. Firefox shows layer: annotations. CSSLayerBlockRule / CSSLayerStatementRule available in JS. |
| Vendor sandboxing | @import url('library.css') layer(vendor) makes the library's !important declarations the lowest priority, and any zero-specificity rule in a higher layer beats all its selectors. |
Exercises
- Layer the cascade proof: Create a stylesheet with
@layer base, components, utils. In base write h1 { color: red }, in components write .hero h1 { color: blue } (higher specificity), and in utils write .text-green { color: green } (lower specificity). Apply all three classes to one element. Confirm green wins. Then remove the layer order statement and observe how priority collapses to specificity-based resolution.
- Vendor sandboxing: Find or create a small CSS library file with at least one
!important declaration. Import it with @import url() layer(vendor). Write a normal utility class in an unlayered stylesheet that targets the same property. Confirm your utility wins despite the library's !important — and explain in a comment why this works using the reversal rule.
- Nested layer component: Build a button component using three nested layers inside
@layer components: base (structure and colour), variants (.btn--ghost, .btn--danger), and states (:hover, :focus-visible, :disabled). Write a rule in the base sub-layer that conflicts with one in states. Verify the correct sub-layer wins by source position.
- revert-layer in practice: Create a
@layer base that sets a { color: #58a6ff; text-decoration: underline }. Create a @layer components that sets .nav a { color: #fff; text-decoration: none }. Add a .nav__link--plain rule inside components that uses revert-layer on both properties. Confirm the link inside .nav with the plain class picks up the base layer values.
- DevTools layer audit: Load any page that uses @layer (or add layers to one of your own). Open Chrome DevTools, inspect an element, and use the Layers pane to list all layers in priority order. Find one rule that is crossed out due to a layer conflict (not a specificity conflict) — identify the winning layer and rule. Document the full dotted layer path of both the winner and the loser.
Next: Chapter 2 — Container Queries.
Size-based and style-based container queries in depth — container-type, container-name,
@container syntax, cqw/cqh/cqi/cqb units, the containment model, style queries with
custom properties, and patterns for truly component-driven responsive design.