Best Practices for CSS Scope in Angular Applications
The new @scope CSS at-rule has been supported by all major browsers since December 2025. This
awesome new feature makes it super easy to scope styles to specific DOM subtrees, like the content of a component.
At work, I mostly implement web applications with the Angular framework. The last few months, I started to ditch Angular’s default view encapsulation and use the native CSS feature instead. Now I’d like to share my learnings and some best practices with you.
Photo: © Aditya Aiyar / pexels.com
If you’re unfamiliar with CSS scope, then read my article
“Will the CSS Scope Feature replace Angular’s View Encapsulation?” first. It explains the
basics of the @scope CSS at-rule and how to use it in Angular.
Demo Project: CSS Scope Sandbox
I’ve created a project with two Angular applications that display
the same content. The only difference is: The app-without-css-scope uses the standard view encapsulation
provided by the Angular framework. On the other hand, the app-with-css-scope disables this feature and
uses the @scope CSS at-rule instead.
Here’s the deployed application with CSS scope. Go ahead and inspect it using your browser’s developer tools:
I love how clean and tidy the DOM looks. It’s way easier to understand the page structure and inspect the styles of an element.
The CSS selectors you see in the browser’s dev tools match those you defined in your source code. No more weird
additions of custom attributes like _ngcontent-xxx .
So, what are the best practices for CSS scope that I’d recommend?
Best Practice 1: Use efficient Donut Scopes
First of all, we need to disable view encapsulation for each of our components. Here’s an example from my demo:
@Component({
selector: 'app-beer-item-list',
encapsulation: ViewEncapsulation.None,
...
})
export class BeerItemList { ... }
Now the CSS selectors won’t be extended with custom attributes and the component styles are applied globally.
Next, we need to use the @scope at-rule instead to encapsulate the styles.
@scope (app-beer-item-list) {
:scope {
display: grid;
...
}
}
This defines the component’s container element app-beer-item-list as
the scoping root which determines the upper boundary of the subtree we want to target.
But what about the content rendered by the child components in the template? The BeerItemList
component includes the child component <app-beer-item-details> in its template. We don’t want
the parent component’s styles to affect the content of its children.
The @scope at-rule allows us to define a scoping limit which determines the
lower boundary. This type of scoping – with an upper and lower boundary – is called a
donut scope:
@scope (app-beer-item-list) to (app-beer-item-details > *) {
:scope {
display: grid;
...
}
app-beer-item-details {
background: var(--canvas-bg-color);
...
}
}
This scope includes the <app-beer-item-details> container element but excludes the
content of the child component.
What happens when you’re styling a component that includes several child components in its template? For example, the
app component in my demo includes the child components AppFooter , AppHeader
and BeerItemList . You would have to define each child component as a scoping limit, which would
get annoying really fast.
To make using CSS scope easier, I’ve created the custom directive CustomNgHostDirective . The
directive adds the custom attribute data-ng-host to a component’s container HTML element. Simply
apply the directive to a component with the hostDirectives property:
@Component({
selector: 'app-beer-item-details',
encapsulation: ViewEncapsulation.None,
hostDirectives: [CustomNgHostDirective],
...
})
export class BeerItemDetails { ... }
Now we can use the data-ng-host attribute for an efficient definition of the lower boundary
of an @scope at-rule. For example:
@scope (app-beer-item-list) to ([data-ng-host] > *) {
:scope {
display: grid;
...
}
app-beer-item-details {
background: var(--canvas-bg-color);
...
}
}
Best Practice 2: Go deep without ::ng-deep
When using Angular’s view encapsulation, component styles normally apply only to the HTML in the component’s own template.
You can use the ::ng-deep pseudo-class to disable view encapsulation for a specific rule.
But this pseudo-class has been deprecated for
a while and you’re not supposed to use it anymore.
With CSS scope, you’re in control again! You can define the scope in a way that lets you safely override, e.g., the inner styles of a 3rd party library.
For example: Let’s say you’re using an Angular Material table to display tabular data. You’re mostly happy with the design but would like to tweak the background colors a bit. Here’s what your component’s styles could look like:
@scope (app-fancy-table) {
table[mat-table] {
thead tr {
background-color: lightblue;
}
}
}
Best Practice 3: Define shared styles as Global CSS Classes
Not all your styles have to be defined as scoped styles in the CSS file of a component. You’ll want to put shared styles
in global files, e.g. in your src/styles folder, and provide them for the whole application.
I personally prefer to create SCSS files with general styles for each type of content or element.
For example: _details.scss , _forms.scss and _table.scss .
They contain styles for specific HTML selectors like, e.g., details , as well as global CSS
classes like, e.g., .default-expansion-panel .
To make it easy for components to override these general styles, if necessary, I put them in a dedicated cascade layer:
@use "sass:meta";
@layer reset, general, components;
@layer general {
@include meta.load-css("base/details");
@include meta.load-css("base/forms");
@include meta.load-css("base/table");
}
Learn more about cascade layers in my article “Using CSS Cascade Layers in Angular”.
As an alternative, you might prefer to use SCSS features like placeholders
or mixins to create reusable styles. I can confirm that including a mixin
with @include also works within the context of the native @scope at-rule.
Unfortunately, using an SCSS placeholder with @extend didn’t work for me in the context of CSS scope.
The SCSS preprocessor apparently doesn’t account for the @scope at-rule yet, resulting in unscoped
styles. At least that was the case for me with Angular 21. Maybe it will be fixed in the future.
Conclusion
As I’ve shown you, it’s pretty easy to switch off Angular’s view encapsulation and define component styles inside
a @scope at-rule. The generated HTML and CSS code has better readability and is far
easier to debug.
Or in other words: Angular’s view encapsulation is dead! Long live CSS scope! 🤩
Posted on