Day 102: selecting the scoping root

posted on

It’s time to get me up to speed with modern CSS. There’s so much new in CSS that I know too little about. To change that I’ve started #100DaysOfMoreOrLessModernCSS. Why more or less modern CSS? Because some topics will be about cutting-edge features, while other stuff has been around for quite a while already, but I just have little to no experience with it.


There are different ways of selecting the scoping root inside a @scope rule.

When you use the :scope pseudo-class in a stylesheet, it matches the :root element.

:root {
  border: 10px solid red;
}

:scope {
  border-color: blue;
}

/* -> 10px blue border on the <html> element */

When you use it inside a scope rule, it matches the rule's scoping root.

<div class="wrapper">
  <div class="content">
      the cascade is unavoidable
  </div>
</div>
@scope (.wrapper) {
  :scope {
    border: 5px solid red;
  }
}

/* -> 5px red border on the .wrapper element */

Selectors inside a scope rule can only match elements that are in scope. Selecting .content within the .wrapper scope works:

@scope (.wrapper) {
  .content {
    background: aqua;
  }
}

/* That's like writing .wrapper .content {} */

Selecting .wrapper .content within the .wrapper scope doesn't work:

@scope (.wrapper) {
  .wrapper .content {
    background: aqua;
  }
}

/* That's like writing .wrapper .wrapper .content {} */

You can use :scope instead of .wrapper. That works because it doesn't match an element with the class .wrapper inside of .wrapper, but the scoping root itself.

@scope (.wrapper) {
  :scope .content {
    background: aqua;
  }
}

/* That's like writing .wrapper .content {} */

Instead of :scope, you can also use &.

@scope (.wrapper) {
  & {
    border: 5px solid orange;
  }

  & .content {
    background: aqua;
  }
}

/* 
  -> 5px orange border on the .wrapper element 
  and aqua background on .content.
*/

There are two differences between :scope and & in this context. They're only evident if you have a list of scoping roots.

The first difference in specificity. :scope has the specificity of a pseudo-class. & takes on the specificity of the most specific selector in the selector list of scoping roots. In the following example :scope overrules & because & has the specificity of a tag selector.

<section id="section">
  <h2>
    <span>yo!</span>
  </h2>

  <p>
    <span>yo!</span>
  </p>
</section>
@scope (section, p) {
  :scope {
    border: 10px solid green;
  }

  & {
    border: 10px solid red;
  }
}

/* -> 10px green border on section and p */

If you scope the section via its id instead of the tag, & takes on the specificity of an id selector and thus overrules :scope.

@scope (#section, p) {
  & {
    border: 10px solid red;
  }

  :scope {
    border: 10px solid green;
  }
}

/* -> 10px red border on the section and p */

The second difference is that :scope only matches the scoping root itself. & can match any element that is matched by the selector list.

<section>
  <h2>
    <span>yo!</span>
  </h2>

  <p>
    <span>yo!</span>
  </p>
</section>
@scope (section, p) {
  :scope span { background: fuchsia; }
  /* 
    section span { }
    p span { }

    -> fuchsia background on span within h2 and p 
  */

  :scope & span { background: aqua; }
  /* 
    section p span { }

    -> aqua background only on span within p 
  */

  :scope :scope span { background: red }
  /* 
    Doesn't match any element because `@scope (section, p)` only
    defines multiple scopes, it doesn't nest them. 
  */
}

Essentially, that means that :scope can only match a scoping root and & can match an element in the selector list, regardless of whether it's considered a scoping root in that context. At least, that's how I interpret it. The spec is still pretty fucking hard to read.

To try out @scope you have to download Chrome Canary and enable the Experimental Web Platform features flag in chrome://flags/.

See on CodePen

Further reading

Overview: 100 Days Of More Or Less Modern CSS