Back to Blog

Cascade Layers: End the Specificity Wars

The Specificity Problem

Most CSS specificity problems are not actually specificity problems. The selectors are fine. The trouble is that the cascade has no concept of intent — no way to say "this rule is a reset, that rule is a utility class, and utilities should always win." When a reset and a component rule have the same specificity, source order becomes the tiebreaker. When a third-party library ships rules you cannot predict, you start reaching for !important to climb back over them. When that escalates, you start writing .parent .container .widget button.submit just to beat a library selector two levels deep.

Cascade layers fix this by separating priority from specificity entirely. You declare which layers exist and what order they sit in. Styles in a higher-priority layer win regardless of how specific the lower-priority selector is. The escalation stops because the architecture does not require it. This is the fifth post in the native web series, following <dialog> for modals, the Popover API for menus, native form validation, container queries, and :has() for relational state.

Browser Support

CSS cascade layers (@layer) are Baseline Widely Available. All major browsers shipped support within a two-month window: Chrome 99 and Firefox 97 in February 2022, Safari 15.4 in March 2022, Edge 100 in April 2022. Global coverage exceeds 93% as of 2026. No polyfill is required.

How Layers Work

The core idea is a two-step declaration: state which layers exist and what order they sit in, then assign CSS rules to those layers. Layer order determines precedence. Later-declared layers win over earlier ones.

/ Step 1 — declare layer order upfront /
@layer reset, base, components, utilities;

/ Step 2 — assign rules to layers /
@layer reset {
  , ::before, *::after { box-sizing: border-box; }
  body { margin: 0; }
}

@layer base {
  body { font-family: system-ui, sans-serif; line-height: 1.5; }
  h1, h2, h3 { line-height: 1.2; }
}

@layer components {
  .btn {
    display: inline-flex;
    padding: 0.5rem 1rem;
    background: #3b82f6;
    color: white;
    border-radius: 4px;
    border: none;
    cursor: pointer;
  }
}

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

The @layer reset, base, components, utilities; declaration at the top sets the precedence order. utilities is last, so it has the highest priority. A one-class utility rule overrides a heavily nested component rule — not because of specificity, but because of layer order. The declaration is the contract.

You can split rules across a layer across multiple blocks. The layer name is the identifier: rules written later in the source file are still part of the same layer, not a higher-priority one.

@layer components {
  .btn { background: #3b82f6; }
}

/ Later in the file, still part of components /
@layer components {
  .btn--danger { background: #dc2626; }
}

Pattern: Taming Third-Party CSS

The most immediate production win is bringing external CSS under layer control. Without layers, a third-party stylesheet's rules compete with your own using specificity — and you lose whenever their selectors are more specific than yours. With layers, you import their CSS into a named layer at whatever priority you choose.

/ Declare layers first — order determines priority /
@layer reset, vendor, base, components, utilities;

/ Import normalize into the reset layer /
@import url('normalize.css') layer(reset);

/ Import a UI library into vendor — below your own styles /
@import url('somelib.css') layer(vendor);

@layer base {
  / Your base styles always win over vendor /
}

@layer components {
  / Your components always win over vendor /
}

@layer utilities {
  / Utilities win over everything /
  .sr-only { position: absolute; width: 1px; ... }
}

The library is in vendor. Your styles are in components and utilities. Because components and utilities are declared after vendor, they win — regardless of how specific the library's selectors are. No !important, no forking the library, no deeply nested override selectors.

This is the pattern Tailwind CSS uses internally. Tailwind organizes its own output into base, components, and utilities layers so your custom rules can sit cleanly above or below framework styles without per-class specificity management.

Pattern: Design System Architecture

For a design system or component library, layers map naturally to the layers of intent already in your CSS architecture:

@layer reset, tokens, layout, components, utilities, overrides;

@layer tokens {
  :root {
    --color-primary: #3b82f6;
    --color-danger: #dc2626;
    --radius-base: 4px;
    --font-body: system-ui, sans-serif;
  }
}

@layer layout {
  .container { max-width: 1200px; margin: 0 auto; padding: 0 1rem; }
  .grid { display: grid; gap: 1rem; }
}

@layer components {
  .card {
    border: 1px solid #e5e7eb;
    border-radius: var(--radius-base);
    padding: 1.5rem;
  }
  .btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border-radius: var(--radius-base);
    font-weight: 500;
    cursor: pointer;
  }
}

@layer utilities {
  .hidden { display: none; }
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0,0,0,0);
    white-space: nowrap;
  }
}

@layer overrides {
  / Page-specific or context-specific adjustments /
}

Each layer owns a distinct responsibility. A developer working on components does not need to think about utilities, and a utility added later cannot accidentally lose to a component rule because it lives in a higher-priority layer.

The Unlayered Rule

The most important gotcha: CSS that is not inside any @layer block has higher priority than all layered styles. Unlayered styles sit above the entire layer stack.

@layer components {
  .btn { background: #3b82f6; } / Layered /
}

/ Unlayered — this wins over everything in any layer /
.btn { background: #10b981; }

This behaviour is intentional. It makes incremental adoption safe: you can layer your new CSS while existing unlayered CSS continues to act as overrides, and nothing breaks. But it means that if you are converting an existing codebase to use layers, unlayered legacy rules will outrank your new layer architecture until they are migrated. Keep a consistent rule: everything is in a layer, or nothing is.

!important Reversal

Cascade layers invert the precedence order for !important declarations. Normal rules: later layers win. Important rules: earlier layers win.

@layer base, components;

@layer base {
  .btn { color: red !important; } / This wins /
}

@layer components {
  .btn { color: blue !important; } / This loses /
}

The rationale is that !important is intended to assert an override from a lower-priority context — the way user stylesheets override author stylesheets in the traditional cascade. The reversal preserves that semantic. In practice: avoid !important entirely within your own layer architecture. If you need a rule to win, put it in a higher-priority layer. That is the whole point of having layers.

Gotchas

Specificity still applies within a layer. Layers override specificity between layers, not within them. Inside a single @layer components block, a more specific selector still wins over a less specific one. The specificity rules you know still apply — layers just stop them from leaking across architectural boundaries.

Anonymous layers cannot be targeted later. A block declared as @layer { } with no name is anonymous. Styles can only be appended to a named layer. Anonymous layers are useful for one-off isolation but cannot be extended.

Declare order upfront. If you add styles to a layer before declaring the full layer order, the layer's position is determined by when it first appears. The explicit upfront declaration (@layer reset, base, components, utilities;) removes ambiguity. Put it at the top of your root stylesheet.

Nesting layers uses dot notation. Nested layers give finer-grained control inside a layer: @layer components.forms { } is a sub-layer of components. Sub-layers follow the same ordering rules within their parent.

Start Here

Cascade layers are available in every major browser since early 2022 and have been Baseline Widely Available since March 2022. The MDN @layer reference covers the full syntax including nested layers, anonymous layers, and the complete cascade ordering rules. Smashing Magazine's introduction to cascade layers by Miriam Suzanne is the most thorough walkthrough of the mental model, written by one of the specification's authors. For a design-system focused perspective, CSS-Tricks has a practical guide on integrating layers with component libraries.

The migration path is incremental. Start by wrapping your reset in @layer reset and your utilities in @layer utilities. Everything between them stays unlayered for now. The unlayered middle wins over reset and loses to nothing — the same precedence as before. Add layers as you refactor, and move unlayered rules into the architecture over time.

Next in this series: native CSS nesting — browser-parsed nesting with & that reduces preprocessor dependence and simplifies your build pipeline.