Step-by-Step Guide to Nesting @layer Rules
If you have sibling layers inside a design-system @layer block that are overriding each other in unexpected order, you need to understand nested layers and their inheritance rules — which live under the CSS Cascade Fundamentals & @layer Syntax reference.
Prerequisites
This procedure assumes you understand:
- How default layer ordering rules determine top-level cascade priority.
- The basic
@layerblock and comma-list syntax from understanding @layer declaration order.
Tooling required: any modern browser (Chrome 99+, Firefox 97+, Safari 15.4+) with DevTools open, and optionally @csstools/stylelint-plugin-cascade-layers for automated auditing.
Step 1: Register the Parent Layer at the Stylesheet Root
The browser assigns cascade priority by registration order, not by selector weight or file position. Declare every top-level layer you need in one comma list at the very top of your entry stylesheet, before any rules.
/* WHY: Fixes the priority sequence for the entire stylesheet.
Any layer named here but not yet populated is a safe placeholder —
the browser holds its slot in the stack. */
@layer reset, base, theme, components, utilities;What this does: The browser records five named layers in ascending priority order. utilities will win over reset regardless of selector specificity, and that relationship cannot be accidentally reversed by later declarations.
Unlayered styles — anything outside an
@layerblock — always outrank every layered rule. If you’re consuming a third-party library that has no@layerwrapper, wrap it using@importwithlayer()to pull it into the stack.
Step 2: Declare the Nested Sibling Order Inside the Parent
Do not let the browser auto-create child layers in encounter order. Explicitly fix the sibling sequence for every parent you plan to nest inside.
/* WHY: Pre-declaring the child sequence before any rules prevents
the browser from locking in whatever order it first encounters
a dot-notation rule for these children. */
@layer components.base, components.variants, components.overrides;What this does: Establishes three child layers under components. components.overrides will beat components.variants, which beats components.base — matching the intuitive override intent.
You can also do this inside a block-level parent, which is equivalent:
/* Alternative block form — identical cascade result */
@layer components {
/* WHY: same ordering guarantee, just scoped inside the block */
@layer base, variants, overrides;
}Step 3: Populate Layers Using Block Syntax or Dot Notation
Two syntax forms let you add rules. They are interchangeable but each has a practical home:
Block nesting — best when all of a component’s rules live in one file:
@layer components {
@layer base {
/* WHY: base holds the structural defaults that variants build on top of */
.btn {
display: inline-flex;
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
border-radius: var(--radius-sm);
font: inherit;
}
}
@layer variants {
/* WHY: variants override base geometry/color without touching base rules */
.btn--primary { background: var(--color-primary); color: var(--color-on-primary); }
.btn--ghost { background: transparent; border: 1px solid currentColor; }
}
@layer overrides {
/* WHY: overrides is last so consuming teams can adjust without !important */
.btn--sm { padding: calc(var(--spacing-unit) * 0.5) var(--spacing-unit); }
}
}Dot notation — best when each layer lives in a separate file, all imported into a central entry point:
/* entry.css — imports populate previously-declared nested layers */
@layer reset, base, theme, components, utilities;
@layer components.base, components.variants, components.overrides;
/* WHY: importing here guarantees each file's rules land in the right
slot, even if the files arrive out of network order */
@import url("components/buttons/base.css") layer(components.base);
@import url("components/buttons/variants.css") layer(components.variants);
@import url("components/buttons/overrides.css") layer(components.overrides);Step 4: Verify the Layer Paths in DevTools
Open DevTools, select a button element, and inspect the Styles panel.
- Each
@layerrule carries an annotation like@layer components.overridesdirectly beside the rule block. Confirm the path matches what you registered. - Rules from
components.overridesshould appear abovecomponents.variantsin the cascaded order (higher in the list = higher priority in the resolved cascade). - Switch to the Computed tab. Expand a property like
padding. Chrome 108+ and Firefox 111+ show the winning layer path next to the winning declaration. - To check a lower-priority rule is correctly overridden: hover its value in the Styles panel — it should be struck through when a higher-priority sibling wins.
- If a rule appears under
@layer (anonymous), a layer was created implicitly — go back and add it to the pre-declaration comma list.
Stylelint audit (CI-ready):
# Install
npm install --save-dev stylelint @csstools/stylelint-plugin-cascade-layers
# .stylelintrc.json
{
"plugins": ["@csstools/stylelint-plugin-cascade-layers"],
"rules": {
"csstools/cascade-layer-name-pattern": [true],
"csstools/no-unknown-layers": [true]
}
}Running npx stylelint "**/*.css" will flag any rule in a layer that was not pre-declared — catching implicit creation before it reaches production.
Troubleshooting
A child layer rule is not overriding a sibling as expected.
: Check that the sibling order in your pre-declaration list reflects intent. @layer components.base, components.overrides means overrides wins; reversing the list reverses the result. The declaration list order, not the block position in the file, is authoritative.
Dot-notation rules appear under (anonymous) in DevTools.
: You referenced a nested layer with dot notation before declaring the parent at the top level. Ensure @layer components (or the full comma list including components) appears before any @layer components.base { } block.
!important in a lower-priority child layer is overriding a higher-priority sibling.
: This is correct behavior. The role of !important in layers inverts priority for !important declarations — earlier-declared layers’ !important rules beat later-declared layers’ !important rules. Restructure to avoid !important rather than fighting the inversion.
A third-party stylesheet is winning over your nested component rules.
: The library’s styles are likely unlayered. Wrap the @import in a layer() call to bring them under your stack; otherwise unlayered author styles win regardless of your nested ordering.
Nested layer rules from a CSS-in-JS runtime are arriving in wrong order.
: Runtime injection via document.adoptedStyleSheets or <style> tags does not respect a pre-declared order in a different sheet. Move layer pre-declarations into the same injected sheet, or use build-step concatenation to guarantee a single entry point.
Complete Working Example
Copy this into a single CSS file to see all steps working together. It covers top-level registration, explicit child ordering, block nesting, and dot-notation population from a separate conceptual source.
/* ============================================================
entry.css — self-contained nested @layer demonstration
============================================================ */
/* STEP 1: Fix top-level priority (lowest → highest, left → right).
WHY: Any rule later placed in utilities will beat components
no matter how specific the component selector is. */
@layer reset, base, theme, components, utilities;
/* STEP 2: Fix child sibling order inside components.
WHY: Without this, the browser creates children in encounter
order, which is almost never what you want. */
@layer components.base, components.variants, components.overrides;
/* STEP 3a: Populate reset — normalize UA styles */
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; }
}
/* STEP 3b: Populate base — primitive design tokens */
@layer base {
:root {
--spacing-unit: 0.5rem; /* 8px grid base */
--radius-sm: 4px;
--color-primary: #0055ff;
--color-on-primary: #ffffff;
}
}
/* STEP 3c: Populate theme — semantic token aliases */
@layer theme {
/* WHY: Separating primitives (base) from semantics (theme)
lets you swap themes without touching component rules. */
:root {
--btn-bg: var(--color-primary);
--btn-text: var(--color-on-primary);
}
}
/* STEP 3d: Populate component children via block nesting */
@layer components {
@layer base {
/* WHY: Structural defaults that every button variant inherits */
.btn {
display: inline-flex;
align-items: center;
gap: var(--spacing-unit);
padding: var(--spacing-unit) calc(var(--spacing-unit) * 2);
border: none;
border-radius: var(--radius-sm);
background: var(--btn-bg);
color: var(--btn-text);
font: inherit;
cursor: pointer;
}
}
@layer variants {
/* WHY: Overrides only what differs — no duplication of base rules */
.btn--ghost {
background: transparent;
border: 1px solid currentColor;
color: var(--btn-bg);
}
.btn--sm {
padding: calc(var(--spacing-unit) * 0.5) var(--spacing-unit);
font-size: 0.875em;
}
}
@layer overrides {
/* WHY: A last-resort slot for consuming teams to adjust without
touching the library source or reaching for !important */
.btn[data-size="xs"] {
padding: 2px 6px;
font-size: 0.75em;
}
}
}
/* STEP 3e: Utilities always win — declared last so they trump components */
@layer utilities {
/* WHY: Atomic helpers like .mt-4 must override component spacing */
.mt-4 { margin-top: calc(var(--spacing-unit) * 4) !important; }
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border-width: 0;
}
}Frequently Asked Questions
How does cascade order resolve when multiple nested layers target the same selector?
The parent layer’s position in the top-level stack sets primary priority. Within that parent, the child layer’s sibling registration order breaks ties. Specificity is only used as a final tiebreaker when two competing declarations live in the exact same layer. So .btn in components.overrides beats #submit-btn in components.base, because overrides was registered after base — selector weight is irrelevant.
Can I use dot-notation without pre-declaring the nested layer order?
You can, but the browser will create sibling layers in encounter order, which is almost never your intent. Always pre-declare with a comma list — @layer components.base, components.variants, components.overrides — before you add any rules. This is the same requirement as for top-level declaration order: declare first, populate later.
What happens to unlayered styles when nested @layer rules are active?
Unlayered author styles always outrank every layered rule regardless of nesting depth or specificity. If you import a library without wrapping it in a layer(), its styles will override even your highest-priority nested layer. The fix is to use @import url("lib.css") layer(vendor) so the library enters the named stack and obeys your declared priority.
Related
- Nested Layers and Inheritance — parent cluster covering how layer hierarchy affects cascade resolution and inheritance
- What happens when you omit layer names in CSS — sibling page on anonymous layer behavior and why unnamed layers break deterministic ordering
- How to declare multiple @layer blocks without conflicts — practical patterns for splitting layer rules across files without registration collisions
- The role of
!importantin layers — explains priority inversion for!importantwithin the layer stack - CSS Cascade Fundamentals & @layer Syntax — root reference for the cascade layer model