← Back to Index

Lesson 19

@scope - Proximity-Based Styling

You've learned how to control which styles win via layers (Lesson 11) and specificity (Lesson 14). @scope adds a new dimension: proximity. Styles from a closer scope root beat styles from a farther one - regardless of source order. It also lets you define where styles stop applying, creating true component boundaries in CSS.

The mental model

@scope does two things:

  1. Limits where styles apply - styles inside @scope (.card) only match elements inside .card. Like a built-in descendant selector, but with a defined boundary.
  2. Proximity wins - when two scopes both match an element, the closer ancestor scope root wins. No specificity battle needed.

How it fits in the cascade: Scope proximity is checked AFTER layers and specificity, but BEFORE source order. So if two declarations have the same origin, layer, and specificity - the one from the closer scope root wins.

Cascade resolution: origin → inline → layers → specificity → SCOPE PROXIMITY → source order

Basic @scope - scoping styles to a subtree

The blue text and orange title only apply inside .scope-card - the paragraph outside is unaffected

I'm outside the scope - normal styling.

Card Title (scoped orange)

I'm inside .scope-card - scoped blue styling applies to me.

<!-- HTML -->
<p>I'm outside the scope — normal styling.</p>
<div class="card">
  <h3 class="title">Card Title</h3>
  <p>I'm inside — scoped styling applies.</p>
</div>

/* CSS */
@scope (.card) {
  p { color: blue; font-weight: 500; }
  .title { color: coral; }
}

/* These rules ONLY apply to p and .title elements
   that are descendants of .card. Elements outside .card
   are completely unaffected — no specificity needed to exclude them. */

Scope limits - where styles STOP applying

The killer feature: you can define a lower boundary. Styles apply from the scope root DOWN TO (but not including) the limit:

Blue background applies between .outer and .inner - the paragraph inside .inner is unaffected

I'm between outer and inner - scoped style applies (blue background).

I'm inside .inner - the scope STOPS here. No blue background.

<!-- HTML -->
<div class="outer">
  <p>I get styled (between root and limit).</p>
  <div class="inner">
    <p>I do NOT get styled (past the limit).</p>
  </div>
</div>

/* CSS — scope FROM .outer, STOP AT .inner */
@scope (.outer) to (.inner) {
  p { background: lightblue; padding: 0.5rem; border-radius: 4px; }
}

/* "to" defines the lower boundary.
   Elements AT or INSIDE .inner are excluded from the scope.
   This creates a "donut" — styles apply in between. */

Why scope limits matter: They solve the "nested component" problem. If you have a .card inside a .card, styles for the outer card's children shouldn't bleed into the inner card. With @scope (.card) to (.card), they stop at the boundary of any nested card.

Proximity - closer scope root wins

When an element is inside multiple overlapping scopes, the one with the closer ancestor scope root wins:

The badge is inside both .theme-blue and .theme-coral - the closer one (.theme-coral) wins

theme-blue scope

Blue badge (closest scope is blue)

theme-coral scope (nested inside blue)

Coral badge (closest scope is coral - wins!)
Ad
<!-- HTML -->
<div class="theme-blue">
  <span class="badge">Blue (closest scope is blue)</span>
  <div class="theme-coral">
    <span class="badge">Coral (closest scope is coral — wins!)</span>
  </div>
</div>

/* CSS — two scopes with the same specificity */
@scope (.theme-blue) {
  .badge { background: blue; color: white; }
}

@scope (.theme-coral) {
  .badge { background: coral; color: white; }
}

/* Both rules match the inner badge (it's inside both scopes).
   Same specificity. Same layer.
   Proximity wins: .theme-coral is the CLOSER ancestor → coral wins.
   Without @scope, source order would decide (coral last → coral wins by coincidence).
   With @scope, proximity ALWAYS wins — order doesn't matter. */

The mental model for proximity: "Which scope root is the shortest DOM walk from this element?" That one wins. It's like CSS specificity, but measured in DOM distance rather than selector weight.

@scope vs other scoping approaches

Approach How it works Limitations
BEM naming Convention-based - unique class names prevent clashes Manual, verbose, no lower boundary
Angular ViewEncapsulation Compiler adds unique attributes to scope component styles Framework-specific, can't define lower boundaries, no proximity
Shadow DOM Hard encapsulation boundary - styles can't leak in or out Too strict for many use cases, hard to theme from outside
@scope Native CSS scoping with upper AND lower boundaries + proximity Still relatively new (Chrome 118+, no Firefox yet)

The :scope selector

Inside @scope, the :scope pseudo-class refers to the scope root element itself:

@scope (.card) {
  :scope {
    /* Styles the .card element itself (the root) */
    border: 1px solid gray;
    border-radius: 8px;
  }

  :scope > .title {
    /* Direct children of the scope root */
    font-weight: 600;
  }

  p {
    /* Any p descendant within the scope */
    color: #555;
  }
}

Practical patterns

Component isolation - nested cards don't leak

Outer card has scoped styles that stop at nested cards

Outer card content (styled by outer scope)

Inner card content (NOT styled by outer scope - boundary stops it)

<!-- HTML -->
<div class="card">
  <p>Outer card content</p>
  <div class="card">
    <p>Inner card — outer styles DON'T reach here</p>
  </div>
</div>

/* CSS — scope stops at nested .card boundaries */
@scope (.card) to (.card) {
  p { color: blue; font-weight: 500; }
}

/* The inner .card gets its own scope instance.
   Outer card's p styles don't leak into inner card. */

Theme regions without custom properties

<!-- HTML -->
<section class="light-region">
  <p>Light themed content</p>
</section>
<section class="dark-region">
  <p>Dark themed content</p>
</section>

/* CSS */
@scope (.light-region) {
  :scope { background: white; }
  p { color: #1a1a2e; }
}

@scope (.dark-region) {
  :scope { background: #1a1a2e; }
  p { color: #f0f0f0; }
}

Browser support

@scope: Chrome 118+, Edge 118+, Safari 17.4+. Firefox: not yet supported (as of mid-2025). Progressive enhancement - without support, styles simply apply globally (the scope limits are ignored).

Retrieval check

Question 1

What does the "to" clause in @scope (.card) to (.slot) define?

The element the scope transitions to
The lower boundary - styles stop applying at (and inside) .slot
An animation endpoint for the scope
A selector that inherits from the scope

Question 2

An element is inside both @scope(.blue) and @scope(.red) - same specificity, same layer. Which wins?

Last in source order
The one whose scope root is the closer ancestor (proximity)
The one with more characters in the selector
Neither - they cancel each other out

Question 3

Where does scope proximity sit in the cascade resolution order?

Before layers - it's the first thing checked
Same level as specificity
After specificity, before source order
After source order - it's the final tiebreaker

Question 4

What does :scope refer to inside @scope (.card)?

The :root element
The .card element itself (the scope root)
The nearest positioned ancestor
The element being styled

Question 5

How does @scope (.card) to (.card) prevent style leakage into nested cards?

It removes the nested .card from the DOM
Styles stop at the nested .card boundary - it starts its own scope
It increases specificity so nested cards can't override
It applies Shadow DOM isolation automatically