Kodokon kodokon.com

Cascade layers (@layer) and @scope

Master the two new cascade criteria - layers and scope proximity - to architect your styles without specificity wars.

10 min · 3 questions

Open this lesson in Kodokon

The cascade resolves conflicting declarations using ordered criteria: origin and importance, then context, then style attribute styles, then layers (@layer), then specificity, then order of appearance. The crucial point: layers are evaluated before specificity. A .button rule in a higher-priority layer therefore beats #nav div.menu a.button in a lower-priority layer - its specificity of (1,2,2) counts for nothing against layer order. That is what makes @layer so powerful for architecting a design system: priority is declared once, structurally, instead of being fought for selector by selector.

CSS
@layer reset, base, components, utilities;

@layer components {
  .button { background: rebeccapurple; }
}

@layer base {
  button { background: gray; }
}
The upfront declaration fixes the order: components beats base, regardless of the order of the blocks in the file.

Two spec subtleties you absolutely need to know. One: styles outside any layer win against layered styles (for normal declarations) - they form an implicit layer placed last. Your local overrides therefore always beat the layered library. Two: with !important, the order fully inverts. The first layers declared win, and an !important inside a layer beats an !important outside any layer. An !important declaration in your reset layer becomes almost unbeatable.

CSS
@layer base, theme;

@layer base {
  a { color: black !important; }
}

@layer theme {
  a { color: purple !important; }
}
With !important, the order inverts: black wins because base is declared first.

@scope bounds a set of rules between a scoping root and an optional lower boundary: @scope (.card) to (.card__content) targets elements that are descendants of .card but outside .card__content - the famous donut scope, ideal for styling a component's frame without leaking into its injected content. Inside, :scope refers to the root itself. And @scope introduces a brand-new cascade criterion: proximity. At equal specificity, the rule whose scoping root is closest to the element wins - something neither BEM nor combinators could ever express.

CSS
@scope (.card) to (.card__content) {
  :scope {
    padding: 1rem;
    border: 1px solid oklch(0.85 0.02 260);
  }
  img { border-radius: 8px; }
}
The img elements in .card's frame are targeted; those injected into .card__content are out of scope.

Knowledge check

Make sure you remember the key points of this lesson.

  1. With @layer a, b; and two normal declarations of equal specificity, one in a and one in b, which one applies?
    • The one in layer a, declared first
    • The one in layer b, because the last layer declared wins
    • The one appearing latest in the file, layers ignored
  2. A normal declaration outside any layer faces a normal declaration placed in a layer. Outcome?
    • The layered declaration always wins
    • The unlayered declaration wins
    • Selector specificity decides
    • The browser reports a conflict
  3. In @scope (.card) to (.card__content), which elements can the inner rules target?
    • All descendants of .card without exception
    • The descendants of .card located outside .card__content (donut scope)
    • Only .card__content and its children