Tags: web-dev concept

Cascade Layers

Date: 2026-09-27


@layer gives you an explicit override order that outranks specificity. Declare the order once at the top, and a one-class selector in utilities beats an ID selector in components — so nobody has to escalate selectors to win.


Cascade layers are named buckets of CSS with an author-declared precedence, checked by the cascade before specificity. Within a layer the usual specificity and source-order rules still apply; between layers, only the layer order counts — The Cascade and Specificity.

The rule, in one block

/* 1. declare the order once. later = stronger */
@layer reset, base, components, utilities;
 
@layer components {
  #checkout .button { background: navy; }   /* (1, 1, 0) */
}
 
@layer utilities {
  .bg-red { background: red; }               /* (0, 1, 0) — WINS */
}
 
.button { background: green; }               /* unlayered — beats BOTH */

Three things to notice:

  • The order line decides everything. A layer’s position is fixed by its first mention, so the one-line declaration up front is what makes the order deliberate rather than an accident of file order
  • Specificity never crosses a layer boundary. The ID in components loses to one class in utilities
  • Unlayered styles sit above every layer. The easy one to miss, and it’s a deliberate choice — adopt layers gradually, and anything you haven’t moved in yet still wins

Where things go

weakest   @layer reset         normalise, box-sizing
          @layer base          element defaults: body, a, h1–h6
          @layer vendor        third-party CSS, imported into a layer
          @layer components    .card, .modal
          @layer utilities     .visually-hidden, .mt-4
strongest (unlayered)          overrides, legacy CSS not yet migrated

Third-party CSS is the best argument for layers. A library with heavy selectors, imported into a low layer, becomes overridable by any single class of yours:

@import url("datepicker.css") layer(vendor);

Before layers the options were matching its specificity or reaching for !important.

!important runs backwards

For !important declarations the layer order reverses: the earliest layer’s !important wins. It’s deliberate — a reset can mark something as non-negotiable and no later layer can undo it — but it’s the opposite of what everyone expects, and it means !important in utilities is weaker than !important in reset.

Tradeoffs and failure modes

  • Unlayered legacy CSS wins by default. Layer your new component CSS while old global CSS stays unlayered, and the old CSS beats it everywhere. Migration usually means wrapping the legacy CSS in a low layer first, not last
  • Nested layers (@layer components.forms) are ordered inside their parent. Useful; also a second ordering to keep in your head
  • Frameworks may emit layers of their own — a utility framework’s generated CSS might arrive already layered, and its order has to fit yours [CHECK: whether the utility framework in use emits native @layer blocks and in what order]
  • It doesn’t fix a wrong model. Layers remove the need to fight specificity; they don’t stop someone writing #page .card inside the component layer

Supported in all current major browsers [CHECK: confirm against current Baseline data if an older-browser audience matters]. Where it isn’t, a layered stylesheet doesn’t partly work — the whole @layer block is ignored, so ship a fallback or don’t layer critical styles — Browser Compatibility.

Where it connects