4 min read

Introducing CSS @scope—it’s finally here!

Table of Contents

Background

With Safari’s TP version now supporting the CSS @scope rule, it’s inevitable that this new feature will become a major asset on the web in the near future.

p

So, what is the @scope rule used for?


The Syntax and Purpose of @scope

If you’ve used precompiled languages like Sass or Less, this syntax should be familiar.

p

The purpose of @scope is actually similar; it enables the use of nested CSS selectors.

For example, here is an HTML and CSS code snippet:

<nav>
    <ul>
      <li><a href="">link 1</a></li>
      <li><a href="">link 2</a></li>
      <li><a href="">link 3</a></li>
    </ul>
  </nav>
  <p><a>out of nav</a></p>  
@scope(nav) {
  ul {
    list-style: none;
    padding: 0;margin: 0;
  }
  li {
    display: inline-block;
  }
  a {
    display: block;
    padding: 6px 12px;
    text-decoration: none;
    background: skyblue;
  }
}

The rendering effect is shown in the image below. As you can see, only the <a> elements inside the <nav> tag have the background color and other styles, while those outside retain the default styles.

p

Priority

The specificity of selectors inside the @scope rule is considered, meaning that in the following CSS code, the priority of the <a> element is the same as that of nav a.

@scope(nav) {
  a {
    …
  }
}

The result shown in the image below also proves this point. If the selectors inside @scope() didn’t affect specificity, then a {background: lightpink} would override the background color set for the <a> element inside the @scope rule. Since this doesn’t happen, it confirms that selectors within @scope() are included in the specificity calculation.

p

Self-Match

Along with the @scope rule comes the :scope pseudo-class, which can match the elements selected by the @scope function.

For example:

@scope(nav) {
  :scope {
    border: double red;
  }
  ...
}

p

Use the to syntax for exclusion

If you want an element within the scope to be excluded from the selector match, you can use the to(xxx) syntax. For example, here is the HTML code:

<nav>
    <ul>
      <li><a href="">link 1</a></li>
      <li><a href="">link 2</a></li>
      <li><a href="">link 3</a></li>
    </ul>
  </nav>

If you don’t want the <a> elements inside the <p> element to be affected by the style, you can set it like this:

@scope(nav) to (p) {
  :scope {
    border: double red;
  }
  ul {
    list-style: none;
    padding: 0;margin: 0;
  }
  li {
    display: inline-block;
  }
  a {
    display: block;
    padding: 6px 12px;
    text-decoration: none;
    background: skyblue;
  }
}

Support for advanced selectors

Continuing from the previous HTML code, if the @scope rule is set like this:

@scope(nav:has(p)) to (p, [class], .some-class) {
  ...
}

It still doesn’t impact the final CSS interpretation.


Conclusion

From the document above, it can be seen that @scope is essentially syntactic sugar, simplifying the syntax and making the hierarchy clearer. It is particularly useful in module or component development.

In other words, the previously used selector naming can now be optimized as follows:

.nav-list-x{}
.nav-list-ul{}
.nav-list-item{}
.nav-list-info{}
.nav-list-img{}
.nav-list-opt{}

Now, you can streamline it like this:

@scope(.nav-list-x) {
  :scope{}
  .ul{}
  .item{}
  .info{}
  .img{}
  .opt{}
}

Hmm, it looks pretty good.

However, it’s important to note that what @scope achieves is not the same as CSS scoping. Scoping, in this context, means that no matter how the CSS selectors are written inside, they won’t affect the outside CSS. This functionality currently only exists within the Shadow DOM.

Therefore, even after @scope becomes widely adopted, the technique used by Vue and React—automatically adding random suffixes to class names to implement localized CSS—will still have its place. However, it won’t be as prevalent as it is now.

Alright, that’s all for today. The web keeps evolving, and frontend learning never stops.