Understanding new CSS at-rule @scope

Lakshmikanth Vallampati
5 min readJan 26, 2024

--

When composing templates, developers often face a struggle to balance specificity and flexibility. On one side, the aim is to be specific about the elements selected, while on the other, the selectors to be easily overridden and not closely tied to the DOM structure.

Over time, developers have crafted and found different solutions and workarounds, for example:

  • Certain libraries eliminate selectors entirely, requiring you to embed styling triggers directly in the markup.
  • Using CSS methodoligies like SMACSS, BEM etc.. helps create a clear and consistent naming structure, making it easier to understand the relationships between different parts of the code and facilitating the maintenance of large projects.
  • JavaScript-based solutions like Scoped CSS or Styled Components rewrite selectors by appending randomly generated strings (e.g., sc-596d7e0e-4) to prevent unintended targeting of elements elsewhere on your page.

But, What if I told you that you don’t need any of those? What if CSS offers a way to be specific about the selected elements without requiring high specificity or tight coupling to your DOM?

Yes. A newly introduced CSS at-rule @scope comes into play, offering you a way to select elements only within a subtree of your DOM.

Introdcuing @scope

@scope enables you to select elements in specific DOM subtrees, targeting elements precisely without writing overly-specific selectors that are hard to override, and without coupling your selectors too tightly to the DOM structure.

Let’s try to understand the @scope concept with a simple example:

In the above html, if you want to apply some specific styles to the img element which is inside the .card component, you can’t set the selector as img, because that would select all image elements across your page. Also, writing a CSS selector like .card > img will be relying on the direct child combinator it is tightly coupled to the DOM structure. Should the markup ever change, you need to change your CSS as well.

Here, it’s important that you find the right balance to only target the elements that you need, without implicitly coupling your DOM structure to your CSS.

With @scope you can limit the reach of your selectors. To target only the <img> elements in the .card component, you set .card as the scoping root of the @scope at-rule.

The result will be as shown below:

You can also set the boundaries to the@scope. Let’s say if you have the template like below:

And you want to target the <img> elements that are placed between .card and .card-contentelements.

With this, it’ll only focus on the elements that are placed inside .card, but not inside .card_content. You can see the result below:

Let’s take another scenario.

For example, having the following HTML:

To target only the <p> elements which are inside the .alpha and .beta components, you should set .alpha and .beta as the scoping root of the @scope at-rule as below:

Results in the styles being scoped to the .alpha and .beta contexts.

The :scope is a special pseudo-class that points to the actual alpha or beta elements.

Some other recent features overlap with scope in interesting ways:

  • Use @layer to group and prioritize different styling concerns. They tend to be more broad and architectural – resets, frameworks, design systems, and utilities – and apply to any number of components. A button component and a theme component might both start with ‘default’ layers.
  • Scope roots will often be good container elements for @container queries. I expect it will be common to see :scope { container: my-scope-name / inline-size; } at the top of many @scope rules.

Wrapping up

  • @scope allows styles to be isolated within specific DOM subtrees.
  • @scope reduces the need for overly specific selectors.
  • Certain properties that you define in your @scope that inherit down to children will still inherit, beyond the lower bound of the@scope.
  • Use @scope to define specific patterns or components and their block-element relationships. Those can be broad (entire themes) or narrow (single buttons) and might overlap sometimes. It’s fine for elements to belong in multiple scopes, but each scope is specific to some fragment of the DOM.
  • Nesting exists to make complex selectors more readable. It can handle a wider variety of selectors than @scope because it is not designed for one specific use-case. While @scope is excellent at expressing the block-element relationship, nesting is still the clear choice for the element--modifier relationships, like pseudo-classes.

References

--

--