Skip to content

Cascade Layers

advanced11 min read

Specificity Wars Are Over

Before cascade layers, controlling CSS priority meant managing specificity — adding classes, avoiding IDs, using BEM, fighting !important chains. Layers solve this fundamentally: you declare priority at the architectural level, and specificity becomes irrelevant between layers.

The declaration order of @layer determines priority, not the specificity of selectors within those layers. A single class selector in a higher-priority layer beats an ID+class+element selector in a lower-priority layer.

Mental Model

Think of cascade layers as floors in a building. The ground floor (first declared layer) has the lowest priority. The top floor (last declared layer) has the highest. Specificity battles only happen within the same floor. A resident on the 10th floor always wins over a resident on the 1st floor, regardless of which apartment number (specificity) they have.

Declaring and Using Layers

Basic Layer Declaration

/* Declare layer order up front */
@layer reset, base, components, utilities;

/* Fill layers anywhere in your CSS */
@layer reset {
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
  }
}

@layer base {
  body { font-family: system-ui; color: var(--text); }
  a { color: var(--link); }
}

@layer components {
  .card { padding: 1.5rem; border-radius: 8px; }
  .btn { padding: 0.75rem 1.5rem; }
}

@layer utilities {
  .text-center { text-align: center; }
  .mt-4 { margin-top: 1rem; }
}

Priority order: utilities > components > base > reset

Importing Into Layers

/* Put third-party CSS into a layer at import time */
@import 'normalize.css' layer(reset);
@import 'component-library.css' layer(vendor);

/* Declare the full order */
@layer reset, vendor, base, components, utilities;

Anonymous Layers

/* Unnamed layer — can't be appended to later */
@layer {
  .temporary-fix { display: none; }
}

Nested Layers

@layer framework {
  @layer base {
    /* framework.base — lowest in framework */
  }
  @layer components {
    /* framework.components — higher in framework */
  }
}

/* Reference nested layers with dot notation */
@layer framework.base {
  /* Append to the nested layer */
}

Layer Priority Rules

Declared Order = Priority

@layer A, B, C;
/* Priority: C > B > A (last declared wins) */

Unlayered Beats All Layers

@layer base { .btn { color: blue; } }

/* Unlayered — highest priority among normal author styles */
.btn { color: red; } /* This wins */
Common Trap

This means if you layer YOUR code but leave third-party code unlayered, the third-party code wins. The intended pattern is the reverse: put third-party code in layers (to contain it) and keep your overrides unlayered (or in higher-priority layers).

!important Reverses Layer Order

Normal declarations: last layer wins. !important declarations: first layer wins (priority reverses).

@layer reset, base, utilities;

@layer reset {
  a { color: blue !important; } /* Wins among !important — it's in the first layer */
}

@layer utilities {
  .text-red { color: red !important; } /* Loses to reset's !important */
}

This ensures foundational layers (resets, accessibility) can protect their !important rules from being overridden by higher layers.

Why !important reverses layer order

The cascade's design principle is that lower-origin declarations can protect their critical rules. For normal declarations, higher layers override lower ones. But for !important, the intent is to protect — so lower layers' !important rules get higher priority. This mirrors how user !important beats author !important at the origin level.

Production Architecture with Layers

@layer reset, vendor, tokens, base, layouts, components, features, utilities;

@layer reset {
  /* Box-sizing, margin reset, minimal normalization */
}

@layer vendor {
  /* Third-party libraries — contained at low priority */
  @import 'headless-ui.css';
  @import 'syntax-highlighter.css';
}

@layer tokens {
  /* Design tokens as custom properties */
  :root { --color-primary: oklch(0.55 0.19 240); }
}

@layer base {
  /* Element defaults: body, headings, links, lists, tables */
}

@layer layouts {
  /* Page-level layouts: sidebar, grid, holy grail */
}

@layer components {
  /* Reusable components: cards, buttons, modals, forms */
}

@layer features {
  /* Feature-specific styles that override components */
}

@layer utilities {
  /* Utility classes: spacing, alignment, visibility */
}

/* Unlayered: emergency overrides, truly exceptional cases */

Migrating an Existing Codebase

/* Step 1: Wrap existing CSS in a single layer */
@layer legacy {
  @import 'existing-styles.css';
}

/* Step 2: Write new CSS in structured layers above legacy */
@layer legacy, base, components, utilities;

/* Step 3: Gradually move rules from legacy to proper layers */
/* Step 4: Remove empty legacy layer when migration is complete */
Execution Trace
Declare
@layer reset, vendor, app;
Priority: app > vendor > reset
Import
@import 'library.css' layer(vendor)
All library selectors contained in vendor layer
Conflict
vendor: .btn { color: blue; } app: .btn { color: red; }
Same selector, different layers
Resolve
Layer order checked before specificity
app layer has higher priority — red wins
!important
vendor: .btn { color: blue !important; }
!important in vendor still loses to app normal if app is not important
What developers doWhat they should do
Putting your application code in a layer while leaving third-party code unlayered
Unlayered styles beat all layered styles. Third-party code should be contained, not elevated.
Put third-party code in layers. Keep your highest-priority code unlayered or in the highest layer.
Expecting !important in a high-priority layer to beat !important in a low-priority layer
The cascade protects lower layers' !important rules from being overridden by higher layers
!important REVERSES layer order — first declared layer's !important wins
Using anonymous layers for code you might need to extend later
Anonymous layers can't be reopened or appended to from elsewhere in the codebase
Always name your layers unless the code is truly one-off and temporary
Declaring @layer order in multiple places
Multiple @layer declarations create layers in the order they're first encountered. Scattered declarations make priority hard to reason about.
Declare the complete @layer order once at the top of your entry CSS file
Quiz
@layer A, B, C is declared. Which layer has the highest priority for normal (non-!important) declarations?
Quiz
@layer reset, utilities is declared. Both have !important rules for the same property. Which wins?
Quiz
You have @layer vendor { .btn { color: blue; } } and unlayered .btn { color: red; }. Which wins?
Key Rules
  1. 1Last-declared layer has highest priority for normal declarations
  2. 2!important REVERSES layer order — first-declared layer's !important wins
  3. 3Unlayered styles beat all layered styles — use this to your advantage
  4. 4Contain third-party CSS in layers. Keep your overrides unlayered or in the highest layer.
  5. 5Declare the complete @layer order once at the top of your entry CSS file