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.
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 off 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