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. Complete the forward declaration so the components layer wins over the base layer.
    @layer ___;
    
    @layer components {
      .button { background: purple; }
    }
    
    @layer base {
      .button { background: gray; }
    }
    • base, components
    • components, base
    • base components
  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