w3tweaks.com · CSS Tutorial

CSS @layer — Cascade Layers

End the specificity wars — layer order beats selector weight

Tab 1

Reorder the Layers — Watch Specificity Lose

Four layers each style the same button with different colors. Use ▲▼ to reorder — the last layer always wins, regardless of selector specificity. Then flip on the unlayered rule and watch it beat everything.

@layer order (top = first declared = lowest priority)
Each layer's rule uses a DIFFERENT specificity — reset uses an ID (1,0,0), utilities uses a bare element (0,0,1). Layer order still decides.
Result
/* Declare order ONCE, up front — this line is the architecture */
@layer reset, theme, components, utilities;

/* Specificity inside layers no longer matters between layers: */
@layer reset { #app button#main.btn { background: slate; } } ← (2,1,2)!
@layer utilities { button { background: amber; } }      ← (0,0,1) WINS
The #1 gotcha — unlayered styles beat ALL layers. A bare button { } rule outside any layer defeats every layered rule, no matter the layer or specificity. This is by design (it enables incremental migration) but it's the top source of "my Tailwind v4 utilities stopped working" bugs — one unlayered reset in globals.css silently overrides utilities living inside layers.
Tab 2

The !important Reversal — The Part Nobody Expects

With normal declarations, later layers win. Add !important and the entire order flips: earlier layers win. This is the spec's genius — it lets low-priority layers (like resets) protect critical declarations — and the biggest mind-bender in the feature.

Normal declarations
1. Inline style attributestrongest
2. Unlayered styles
3. @layer utilities (last)
4. @layer components
5. @layer theme
6. @layer reset (first)weakest
With !important — REVERSED
1. @layer reset !importantstrongest
2. @layer theme !important
3. @layer components !important
4. @layer utilities !important
5. Unlayered !important
6. Inline !importantweakest*
Live proof — which !important wins?
@layer early, late;

@layer early { .imp-btn { background: seagreen !important; } }
@layer late  { .imp-btn { background: crimson !important; } }

/* Normal rules: late wins. !important rules: EARLY wins.
   The button is seagreen — check it above. */
Why reversal is genius: a design system's low-priority reset layer can mark accessibility-critical declarations !important — and nothing above it, not even unlayered app styles, can casually override them. The layer that yields by default becomes the layer that protects on demand. Rule of thumb: in a layered codebase, !important means "protect this", not "force this".
Tab 3

Production Architecture

The patterns running real 2026 codebases — including how Tailwind v4 is built entirely on @layer.

🏗️ The Standard Layer Stack
/* One line — declare in your entry CSS, FIRST */
@layer reset, tokens, base,
       layouts, components,
       utilities, overrides;

Declare order once, then fill layers anywhere in any file — even out of order. Gotcha: the FIRST declaration wins; a later @layer utilities, components; can't reorder them. Put the master declaration at the very top of your entry stylesheet.

📦 Taming Third-Party CSS
/* Import a framework INTO a low layer */
@import url("bootstrap.css") layer(framework);

@layer framework, custom;

@layer custom {
  .btn-primary { background: #1a73e8; }
  /* beats Bootstrap's .btn-primary regardless
     of Bootstrap's selector specificity */
}

The killer use case: a framework's .container .row .card p monster selectors can never beat your simple .content p again — your layer outranks theirs.

🌬️ Tailwind v4 Runs on @layer
/* Tailwind v4 generates: */
@layer theme { /* design tokens */ }
@layer base { /* preflight resets */ }
@layer components { }
@layer utilities { /* always win */ }

/* YOUR unlayered globals.css: */
a { color: black; } ← beats utilities!

/* Fix: move it into a layer */
@layer base { a { color: black; } }

The #1 Tailwind v4 bug report: unlayered rules in globals.css silently defeating utility classes. DevTools clue: the winning rule shows no layer attribution — a bare selector. Fix: layer everything.

🪆 Nested Layers & Dot Notation
@layer framework {
  @layer theme { /* framework.theme */ }
  @layer components { }
}

/* Address directly with dot notation */
@layer framework.theme {
  :root { --accent: #60a5fa; }
}

/* Import into a sub-layer */
@import url("tw.css") layer(vendor.tailwind);

Sub-layers scope a vendor's internal ordering without polluting your top-level stack. The outer layer's position decides against your layers; inner order is the vendor's business.

⏪ revert-layer — The Escape Hatch
@layer base, special;

@layer base { .card { background: navy; } }

@layer special {
  .card.plain {
    background: revert-layer;
    /* "pretend this layer never set it"
       → falls back to base's navy */
  }
}

revert-layer rolls a property back to whatever lower layers computed — surgical un-styling without knowing or repeating the lower layer's value.

🔄 Incremental Migration
/* Step 1: layer the third-party CSS only */
@import url("vendor.css") layer(vendor);

/* Step 2: your existing CSS stays unlayered
   — it still wins over vendor. Zero breakage. */

/* Step 3: move your files into layers
   one at a time during refactors */

Because unlayered beats layered, migration is safe by default: layer the vendor CSS first (instant specificity-war victory), then migrate your own files gradually. Watch for legacy internal specificity wars shifting winners when wrapped — test per file. Zero runtime cost: layers resolve at parse time.

Browser support: universal — Chrome 99+, Firefox 97+, Safari 15.4+, Edge 99+. Baseline since March 2022, 96%+ global. No fallbacks needed; in truly ancient browsers, rules apply by normal specificity. DevTools tip: Chrome and Firefox both show layer attribution in the Styles panel — an unlayered winner shows a bare selector, which is your debugging smoking gun.