Declaring Multiple @layer Blocks Conflict-Free

When styles from multiple files each create their own @layer blocks without a shared manifest, the browser silently locks cascade priority at the wrong position—causing components to override resets and utilities to lose to base rules, with no specificity conflict to debug.

This page addresses the specific scenario where your layer declaration order is fragmented across files, and shows you exactly how to eliminate those conflicts. It is a focused procedure page under the CSS Cascade Fundamentals & @layer Syntax section.

Prerequisites

  • You understand how @layer establishes cascade priority by declaration order—read that page first if not.
  • You have a project with at least two stylesheets that both use @layer blocks (or you are planning such an architecture).
  • Optional tooling: a bundler (Vite, webpack, or PostCSS), and Stylelint with @csstools/stylelint-plugin-cascade-layers for enforcement.

Why Fragmented @layer Blocks Conflict

Before the procedure, it helps to see exactly what goes wrong. The diagram below shows how the browser assigns cascade positions when there is no manifest versus when there is one.

Fragmented vs manifest-driven @layer cascade order Left panel shows three files each registering layers independently, resulting in wrong order: components(0), utilities(1), reset(2). Right panel shows a manifest file declaring reset, base, theme, components, utilities first, resulting in correct order matching intent. Without manifest (broken) With manifest (correct) components.css @layer components {…} locks slot 0 utilities.css @layer utilities {…} locks slot 1 reset.css @layer reset {…} locks slot 2 — too late! Cascade priority (lowest → highest) components utilities reset slot 0 reset wins over components — wrong! main.css — layer manifest (loaded first) @layer reset, base, theme, components, utilities; all slots locked before any rules reset.css @layer reset components.css @layer components utilities.css @layer utilities Cascade priority (lowest → highest) reset base theme components utilities slot 0 → slot 4 utilities always wins — correct!

Step-by-Step Procedure

Step 1 — Write the Layer Manifest

Create a single @layer statement that names every layer your project uses, in the order you want them resolved (lowest priority first).

/* main.css — this line must be the very first CSS the browser parses.
   Declaration order is cascade priority: reset loses to base, base to theme,
   theme to components, components to utilities. Later = higher priority. */
@layer reset, base, theme, components, utilities;

What this does: The browser immediately locks five named cascade slots. Any @layer block that appears later in any file—regardless of file load order—will map its rules into these pre-assigned slots. No implicit slot creation can occur.


Step 2 — Keep the Manifest Separate from Rule Blocks

Do not mix the manifest declaration with a rule block in the same statement. This is the most common mistake: writing @layer reset { … } as the first block instead of the comma-separated manifest.

/* WRONG — this creates an implicit single-layer registration for 'reset'
   at position 0, before the manifest runs. Every layer declared after
   this will be stacked after reset. If you later add a manifest, reset
   is already locked at 0 regardless of where reset falls in the manifest. */
@layer reset {
  *, *::before, *::after { box-sizing: border-box; }
}

/* RIGHT — separate manifest first, then rule blocks */
@layer reset, base, theme, components, utilities; /* manifest: slots 0–4 */

@layer reset {
  /* Rules assigned here go into the pre-locked slot 0 */
  *, *::before, *::after { box-sizing: border-box; }
}

What this does: Keeping the manifest free of braces guarantees the browser registers all five names in a single parsing step. It cannot partially register them.


Step 3 — Populate Layers in Separate Blocks or @import Statements

With the manifest in place, you can spread rule blocks across as many files as you like. Each named block adds to the same logical layer without changing its cascade position.

/* --- In reset.css (or inline after the manifest) --- */
@layer reset {
  /* These rules land in slot 0, regardless of when this file loads */
  *, *::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
}

/* --- In base.css --- */
@layer base {
  /* Slot 1 — typography and form element defaults */
  body { font: 1rem/1.5 system-ui, sans-serif; }
  h1, h2, h3 { line-height: 1.2; }
}

/* --- In theme.css --- */
@layer theme {
  /* Slot 2 — design tokens; see theme-token-layer-mapping for deep coverage */
  :root {
    --color-surface: #fff;
    --color-text: #111;
    --color-border: #e2e8f0;
  }
}

/* --- In components.css --- */
@layer components {
  /* Slot 3 — component rules; override base and theme freely */
  .card {
    border: 1px solid var(--color-border); /* reads token from theme slot */
    background: var(--color-surface);
  }
}

/* --- In utilities.css --- */
@layer utilities {
  /* Slot 4 — highest priority; always wins over components */
  .p-4 { padding: 1rem; }
  .text-sm { font-size: 0.875rem; }
}

What this does: Each @layer name { } block appends rules to the already-registered slot. Source order within a slot determines which duplicate property wins; cascade slot determines which layer wins.


Step 4 — Use @import layer() for Third-Party Files

When pulling in a stylesheet you don’t control—a reset library, a UI framework, a font provider’s CSS—wrap it in a layer import so it cannot escape your priority model.

/* Assign third-party CSS to the reset slot via @import layer() syntax.
   The library's internal selectors cannot exceed the cascade priority
   of the reset layer, no matter how specific they are. */
@layer reset, base, theme, components, utilities; /* manifest must come first */

@import url("modern-normalize.css") layer(reset);
@import url("design-tokens.css") layer(theme);

/* Your own rule blocks follow. They share the same named slots
   and accumulate rules in source order within each slot. */
@layer components {
  .card { border: 1px solid var(--color-border); }
}

What this does: @import url() layer() is the declarative alternative to wrapping third-party CSS in a @layer block. It is equivalent in effect but cleaner when the source is an external file. Note: @import rules must appear before any non-import CSS, so place them immediately after the manifest.


Step 5 — Pin the Manifest in Your Bundler

In a build pipeline, file concatenation order can move the manifest away from position zero. Explicitly control this.

Vite (vite.config.js)

// Vite resolves the first CSS import as the entry point.
// By importing the manifest file first in main.js, Vite ensures
// it appears at the top of every processed CSS bundle.
import './styles/layers.css';   // contains only: @layer reset, base, theme, components, utilities;
import './styles/reset.css';
import './styles/base.css';
import './styles/components.css';
import './styles/utilities.css';

PostCSS (postcss.config.js)

// @csstools/postcss-cascade-layers polyfills @layer for older browsers
// and will warn if a layer block appears before the manifest.
module.exports = {
  plugins: [
    require('@csstools/postcss-cascade-layers'),
  ],
};

What this does: Pinning the manifest as the first processed asset means the concatenated output always opens with the comma-separated declaration. Bundler HMR or code-split lazy loads that inject CSS later will still honour the original slot order.


Verification

After implementing the manifest pattern, confirm it is working:

Chrome / Edge DevTools

  1. Open DevTools → Elements panel → Styles tab.
  2. Click any element with layered styles.
  3. Scroll to the bottom of the Styles panel — Chrome shows a “Layers” badge listing each @layer name in priority order. Verify the order matches your manifest.
  4. Select a property that should be overridden (e.g. a base font-size beaten by a utilities class). DevTools will show the losing declaration struck through.

Firefox DevTools

  1. Open DevTools → Inspector → Rules tab.
  2. Layered rules display their layer name in a grey badge beside each rule block.
  3. Confirm overriding layers appear lower (higher priority) in the Rules panel.

Stylelint

npm install --save-dev @csstools/stylelint-plugin-cascade-layers
// .stylelintrc.js — prevents implicit layer creation in CI
module.exports = {
  plugins: ['@csstools/stylelint-plugin-cascade-layers'],
  rules: {
    '@csstools/cascade-layers/require-defined-layers': [true, {
      // List every layer name your manifest declares
      layers: ['reset', 'base', 'theme', 'components', 'utilities'],
    }],
  },
};

The require-defined-layers rule errors if any file uses an @layer name that is not in the declared list—catching typos and undeclared layers before they reach production.


Troubleshooting

Layers resolve in wrong order despite manifest : The manifest is not actually loading first. Check that no bundler or concatenation step places another stylesheet’s content before main.css. Search the built CSS output for the first @layer line—it must be the comma-separated manifest, not a rule block.

Unlayered rules override my highest-priority layer : Any CSS rule written outside a @layer block is unlayered author CSS. Unlayered author CSS always wins over all layered rules, regardless of layer priority. Audit for loose rules not wrapped in a layer block. This is the single most common silent override trap.

Duplicate layer name appears with different slot numbers in DevTools : You have two separate manifest-style declarations that assign the same name different positions (e.g. one file says @layer reset, components and another says @layer components, reset). Consolidate to a single manifest—only the first declaration of a name counts, but competing manifests signal a structural problem.

@import layer() rules appear after @layer blocks in build output : @import must precede all non-@import CSS in a stylesheet. If a bundler hoists rule blocks above imports, layer priority for imported files will be computed from an implicit registration point, not the manifest. Use dynamic fetch + CSSStyleSheet insertion, or restructure the build so imports remain at the top.

Stylelint require-defined-layers reports false positives on vendor files : Use the ignore option to exclude node_modules paths, or wrap vendor imports in a single @layer vendor { } block and add vendor to the declared layers list.


Complete Working Example

The following is a self-contained stylesheet you can copy into any project. It implements the full manifest pattern with five layers, an @import for a third-party reset, and sample rules in each layer.

/* ============================================================
   main.css — complete conflict-free @layer architecture
   ============================================================ */

/* STEP 1: Manifest — fixes cascade priority before any rule is parsed.
   Order (lowest → highest): reset → base → theme → components → utilities.
   Add new layer names here when your architecture grows. */
@layer reset, base, theme, components, utilities;

/* STEP 2: Third-party reset assigned to the reset slot.
   Its selectors cannot escape slot 0, no matter how specific. */
@import url("modern-normalize.css") layer(reset);

/* STEP 3: Rule blocks — each name references a pre-declared slot. */

@layer reset {
  /* Supplement the imported reset with project-level resets */
  *, *::before, *::after { box-sizing: border-box; }
}

@layer base {
  /* Typography defaults; intentionally low specificity so theme/components win */
  body {
    font: 1rem/1.5 system-ui, sans-serif;
    color: var(--color-text, #111);
    background: var(--color-surface, #fff);
  }
  h1, h2, h3 { line-height: 1.2; }
  a { color: var(--color-link, #0070f3); }
}

@layer theme {
  /* Design tokens — keep token definitions here so components can read them.
     See /architecture-patterns-design-system-scaling/theme-token-layer-mapping/
     for the full strategy. */
  :root {
    --color-surface: #ffffff;
    --color-text: #111827;
    --color-border: #e2e8f0;
    --color-link: #0070f3;
    --color-primary: #6366f1;
  }
}

@layer components {
  /* Component rules — can freely reference theme tokens.
     Wins over base typography; loses to utilities. */
  .card {
    border: 1px solid var(--color-border);
    border-radius: 0.5rem;
    padding: 1.5rem;
    background: var(--color-surface);
  }

  .btn {
    display: inline-flex;
    align-items: center;
    padding: 0.5rem 1rem;
    background: var(--color-primary);
    color: #fff;
    border-radius: 0.375rem;
    border: none;
    cursor: pointer;
  }
}

@layer utilities {
  /* Utility classes — highest priority layered author CSS.
     Use for single-property overrides that must always win over components. */
  .p-4  { padding: 1rem; }
  .p-6  { padding: 1.5rem; }
  .mt-4 { margin-top: 1rem; }
  .text-sm { font-size: 0.875rem; line-height: 1.25rem; }
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
  }
}

/* IMPORTANT: Any rule written here — outside a @layer block — is unlayered
   author CSS and will win over ALL of the above layers.
   Reserve this space for emergency overrides only, and document each one. */

FAQ

What happens if I declare @layer blocks in different files without a manifest?

The browser locks each layer’s cascade position at its first encounter. If components.css loads before reset.css, the component layer gets a lower priority slot than reset—which is almost certainly wrong. A single upfront manifest prevents this by fixing the order before any rules are parsed.

Can I add rules to the same layer in multiple separate blocks?

Yes. A layer name can appear in many @layer blocks throughout your stylesheets—all rules accumulate into the same logical layer in source order. Only the layer’s cascade priority (its slot in the stack) is fixed by the first declaration; the rules themselves are open to addition at any point.

Does bundler concatenation order matter when I use a layer manifest?

Yes. The manifest @layer statement must appear first in the concatenated output. If a bundler reorders files so that a rule block from components.css precedes the manifest, that block implicitly registers components at position 0 before the manifest can lock the intended order. Pin the manifest file to be the first import or entry point.