Will the CSS Scope Feature replace Angular's View Encapsulation?

We developers love convenience. We welcome any tool or feature that makes our lives easier. This is especially true in regards to styling web content with CSS.

One major challenge is to define CSS rules that target specific elements without writing overly-specific selectors that are hard to override. Also, you don't want to couple your selectors too tightly to the DOM structure as it is prone to change.

Various JavaScript frameworks have come up with different solutions for the problem: React uses CSS Modules, which allow the scoping of CSS by automatically creating a unique class name for a component's styles. In Angular, a component's styles are encapsulated using custom HTML attributes so that they don't affect the rest of the application.

A box with six donuts in different colors. Photo: © cottonbro studio / pexels.com

But why can't we scope styles with CSS alone? Actually, we can! The new @scope CSS at-rule allows you to scope styles to specific DOM subtrees. You can even define lower bounds for the scope, creating a so-called donut scope.

I'll explain the basics of this new CSS feature and test its capabilities with an Angular demo application.

Demo: CSS Scope vs View Encapsulation

My demo application was generated with Angular 17. It includes a header and main section with some paragraphs, links, and several recipe cards.

The app-recipe-card component (yellow background) uses the new CSS scope feature to only target the HTML elements in its own subtree. Important: At the moment of writing this article, scoped styles only work in Chrome, Edge, and Safari!

For comparison, I've also defined the app-recipe-card-old component (blue background), which uses Angular's default view encapsulation. Go ahead and inspect the elements with your browser's developer tools.

As the demo shows, CSS scope enables us to write simple selectors with low specificity and without the need of extra class names. Let's take an in-depth look at how it all works.

The Basics of CSS Scope

The new CSS scope feature is defined in the CSS Cascading and Inheritance Level 6 module, which is still a working draft at the time of writing this post. It states:

A scope is a subtree or fragment of a document, which can be used by selectors for more targeted matching. A scope is formed by determining: The scoping root node, which acts as the upper bound of the scope, and optionally the scoping limit elements, which act as the lower bounds.

In short, you need to define the root node of the DOM subtree you want to target. Optionally, you can also list one or more inner elements that represent the lower boundary of your scope.

The @scope CSS at-rule

The app-recipe-card component in my demo includes headings, paragraphs, and an unordered list. We style them with the following CSS code:

@scope (app-recipe-card) { h3 { color: var(--highlight-color); font-size: 1.3rem; } p { margin-block: 0 1em; } ul { list-style: square; margin: 0; padding-inline-start: 1.25rem; } li::marker{ color: var(--highlight-color); } }

The @scope block above defines app-recipe-card as the scoping root, which determines the upper boundary of the subtree we want to target. Now all contained style rules, like h3 { ... }, can only select from that limited subtree of the DOM.

Creating a Donut Scope

In some cases, only setting a scoping root won't be enough. In Angular or React, you usually nest components within other components to create complex user interfaces. How can you make sure that the parent component's styles don't affect its child components?

The @scope at-rule also accepts a scoping limit which determines the lower boundary. In my demo, the recipe cards are nested within the app-recipes-list component. Here's part of its CSS code:

@scope (app-recipes-list) to (app-recipe-card, app-recipe-card-old) { p { font-style: italic; } }

This way, we only italicize the paragraphs defined by the app-recipes-list parent component. The paragraphs inside the child components app-recipe-card and app-recipe-card-old are not affected.

This type of scoping – with an upper and lower boundary – is called a donut scope. I recommend you also read the article “Limit the reach of your selectors with the CSS @scope at-rule” by Bramus Van Damme. It includes great visualizations of different scope scenarios.

The :scope selector

Another useful feature is the :scope selector. It allows you to target the scoping root element itself inside of the @scope block. Here's an example from my demo:

@scope (app-recipe-card) { :scope { --highlight-color: rgb(194 34 2); display: block; background: lightgoldenrodyellow; color: black; padding: 1rem; } }

Browser Support

What's that? You think that CSS scope is awesome and want to use it right away in all your projects? Unfortunately, you'll have to be patient.

At the moment, the @scope at-rule is only supported by Chrome 118+, Edge 118+, and Safari 17.4+. Firefox doesn't support it yet, but Mozilla is actively working on the feature. Let's hope for cross-browser support sometime in 2024!

How to use @scope in an Angular application

In Angular, a component's styles are encapsulated by default. The framework creates custom HTML attributes like _ngcontent-pmm-6, adds them to the generated HTML elements and modifies the component's CSS selectors so that they are only applied to the component's view.

If you want to use the @scope at-rule instead, you need to manually switch off view encapsulation for each component.

Switching Off View Encapsulation

To disable view encapsulation, you need to set encapsulation to ViewEncapsulation.None in the component's decorator. Here's an example from my demo:

@Component({ selector: 'app-recipe-card', standalone: true, templateUrl: './recipe-card.component.html', styleUrl: './recipe-card.component.css', encapsulation: ViewEncapsulation.None, }) export class RecipeCardComponent { @Input({ required: true }) recipe!: Recipe; }

Now your CSS selectors won't be extended with custom attributes and the component styles are applied globally. You'll need to use the @scope at-rule instead to encapsulate the styles.

Comparing the HTML and CSS output

As we've seen, using CSS scope requires a little bit of effort when creating a component. Simply using Angular's default view encapsulation instead would be more convenient.

But I would argue that the advantages of CSS scope far exceed this minor inconvenience. Let's take a look at the HTML and CSS generated for the app-recipe-card component in my demo:

/* HTML elements in the DOM */ <app-recipe-card> <h3>Pizza Margherita</h3> <p>The best pizza in town!</p> <h4>Ingredients</h4> <ul> <li>Cutting edge CSS features!</li> /* More list items */ </ul> </app-recipe-card> /* Generated CSS code (excerpt) */ @scope (app-recipe-card) { h3 { color: var(--highlight-color); font-size: 1.3rem; } }

Let's compare this to the HTML and CSS generated for the app-recipe-card-old component:

<app-recipe-card-old _nghost-ng-c2291633987> <h3 _ngcontent-ng-c2291633987>Pizza Margherita (Old)</h3> <p _ngcontent-ng-c2291633987>The (second) best pizza in town!</p> <h4 _ngcontent-ng-c2291633987>Ingredients</h4> <ul _ngcontent-ng-c2291633987> <li _ngcontent-ng-c2291633987>DOM cluttering view encapsulation</li> /* More list items */ </ul> </app-recipe-card-old> /* Generated CSS code (excerpt) */ h3[_ngcontent-ng-c2291633987] { color: var(--highlight-color); font-size: 1.3rem; }

Now imagine you need to debug your application. The recipe card isn't rendered the way you intended. You open your browser's developer tools and inspect the DOM and the applied styles. Which code would be easier to read and understand?

I think that the myriad of _ngcontent-ng-c2291633987 attributes would be very distracting. I think that my mind would find it easier to process the @scope (app-recipe-card) { h3 } selector than the h3[_ngcontent-ng-c2291633987] selector. 😉

Conclusion

Getting back to my initial question: Will the CSS scope feature replace Angular's view encapsulation? Maybe. I really don't know. Ideally, the Angular team will adapt their default view encapsulation mechanism to use the @scope at-rule.

Regardless of what the Angular team does: It's already pretty easy to switch of view encapsulation for a component and define the styles inside a @scope block. The generated HTML and CSS code has better readability and is far easier to debug. It also contributes to a reduced bundle size of your web application.

For me, one thing is certain: As soon as CSS scope has cross-browser support, I'll be using it in my projects. 🤩

Posted on