DEV Community

Igor Moto
Igor Moto

Posted on

How ViewEncapsulation and ng-deep work in Angular

Keep default Angular ViewEncapsulation
Many Angular developers and layout designers who write CSS/SCSS code in Angular applications have encountered a situation where they need to apply styles to a component nested in the current one and, without fully understanding how it works, turned off style encapsulation or added ng-deep, while not taking into account some nuances, which later leads to problems. In this article, I will try to present all the details as simply and concisely as possible.
When a component has style encapsulation enabled (it is enabled by default and should be left enabled in most cases), the styles contained in the component's style file\files will only apply to the components' elements. This is very convenient, you don't have to keep track of the uniqueness of the selectors, you don't have to use BEM or come up with long class names and keep them unique, although you can still do that if you want. During the creation of each component, Angular itself will add a unique attribute to all elements inside the component, for example, _ngcontent-ool-c142 and replace your .my-class selector with .my-class[_ngcontent-ool-c142] (this is in the case of ViewEncapsulation.Emulated , which is enabled by default, if you specify ViewEncapsulation.ShadowDom the behavior is different but the result is the same).

Now let's imagine that we have a component named ComponentA

<div class="checkbox-container">
  <mat-checkbox>Check me</mat-checkbox>
</div>
Enter fullscreen mode Exit fullscreen mode

that has a mat-checkbox from Angular material nested inside it (this can be your own component, not necessarily components from libraries).

Inside mat-checkbox there is a label that we want to add a border to.

<mat-checkbox>
  <label>
...
Enter fullscreen mode Exit fullscreen mode

If we write in the component's style file,

mat-checkbox label {
  border: 1px solid #aabbcc;
}
Enter fullscreen mode Exit fullscreen mode

then after applying ViewEncapsulation.Emulated the selector will be something like this

mat-checkbox[_ngcontent-uiq-c101]   label[_ngcontent-uiq-c101] {
  border: 1px solid #aabbcc;
}
Enter fullscreen mode Exit fullscreen mode

i.e. the border will be applied to the label with the _ngcontent-uiq-c101 attribute, but all child elements inside the mat-checkbox will have a different attribute since the label is inside another component and it will either have an attribute with a different ID (id of mat-checkbox component), or it will not exist at all if the component, in turn, has encapsulation disabled (in our case, there will be no attribute at all, because mat-checkbox, like other components from Angular Material has ViewEncapsulation.None).
Thus, styles restricted by the ComponentA component attribute only apply to elements directly inside that component. If the component contains another component, then these styles no longer apply to its elements.
If you're wondering exactly how Angular's Emulated encapsulation works, you can find a lot of detailed articles on the subject, but here I'll give a very brief description so as not to bloat the article. If the component has encapsulation, then the _nghost-ID attribute will be added to the component itself, and the _ngcontent-ID attribute will be added to each nested element, and [_ngcontent-ID] will be added to all styles in this component. This way all styles will be applied ONLY to the elements inside that component.

What if we need to apply styles to the elements inside the nested component (in our example, to the label inside the mat-checkbox)

In order to apply styles, we have three options:

  • disable style encapsulation in ComponentA
  • use ng-deep
  • place css code in global styles (those in styles.(s)css or in other files specified in the styles section in angular.json)

Let's take a closer look at them

ViewEncapsulation.None

In this case, all styles inside the component will become "global", and this will happen only after the component is created, i.e. after the user has visited the section of the application where this component is used, which makes it very difficult to identify this problem. Let's turn off style encapsulation on our component.

@Component({
  selector: 'app-component-a',
  templateUrl: './component-a.component.html',
  styleUrls: ['./component-a.component.scss'],
  encapsulation: ViewEncapsulation.None
})
Enter fullscreen mode Exit fullscreen mode

remember that in the style file we have this

mat-checkbox label {
  border: 1px solid #aabbcc;
}
Enter fullscreen mode Exit fullscreen mode

until the user has opened the page where component A is used, all other mat-checkboxes in the application look borderless, but after component A has rendered, the css code above will be dynamically added to the section in the DOM tree and after then all mat-checkboxes will use these styles.
In order to prevent this apparently undesirable effect, we can limit the scope of styles by applying a more specific selector. For example, let's add the "checkbox-container" class to the mat-checkbox's parent element,

<div class="checkbox-container">
  <mat-checkbox>Check me</mat-checkbox>
</div>

and fix the selector to this

.checkbox-container mat-checkbox label {
  border: 1px solid #aabbcc;
}

now only checkboxes located inside an element with the checkbox-container class will get the border. But instead of adding a class with a unique name and making sure they're not repeatable, it's much easier to use a component selector, because it will be unique

app-component-a mat-checkbox label {
  border: 1px solid #aabbcc;
}

Conclusion: if you turn off encapsulation, don't forget to add the component selector to ALL styles inside the component, in case of SCSS\SASS, just wrap all code in:

app-component-a {
  ...
}

Pseudo-class ng-deep

Now let's turn encapsulation back on by removing encapsulation: ViewEncapsulation.None from the @Component decorator. And add the ::ng-deep selector to the css

::ng-deep mat-checkbox label {
  border: 1px solid #aabbcc;
}

ng-deep will force the framework to generate styles without adding attributes to them, as a result, this code will be added to the DOM:

mat-checkbox label{border:1px solid #aabbcc}

which will affect all mat-checkbox applications, just like if we added it to global styles or turned off encapsulation as we did earlier. To avoid this behavior, we can again restrict the scope to the component selector

::ng-deep app-component-a mat-checkbox label {
  border: 1px solid #aabbcc;
}

or do it even simpler and use the :host pseudo-class

:host ::ng-deep mat-checkbox label {
  border: 1px solid #aabbcc;
}

which is much more convenient and reliable (imagine if you renamed the component selector and forgot to change it in the css code).
How does it work? Very simple - Angular will generate in this case the following styles

[_nghost-qud-c101] mat-checkbox label{border:1px solid #aabbcc}

where _nghost-qud-c101 is the attribute added to our ComponentA, i.e. the border will apply to all labels inside any mat-checkbox lying inside an element with the _nghost-qud-c101 attribute, which ONLY ComponentA has.

<app-component-a _ngcontent-qud-c102 _nghost-qud-c101>

Conclusion: if using ::ng-deep ALWAYS add :host or create a mixin and use it everywhere

@mixin ng-deep {
  :host ::ng-deep {
    @content;
  }
}

@include ng-deep {
  mat-checkbox label {
    border: 1px solid #aabbcc;
  }
}

Many developers are confused by the fact that ng-deep has been marked as deprecated for a long time. The Angular team had plans to deprecate this pseudo-class, but that decision was later shelved indefinitely, at least until new alternatives come along. If we compare ng-deep and ViewEncapsulation.None, then in the first case, we at least turn off encapsulation not for all component styles, but only for those that we need. Even if you have a component where all the styles are for child components, ng-deep seems to be more advantageous because you can later add styles for the component's own elements, in which case you just write them above/below the code nested in :host ::ng-deep {} and they will work as usual, but with encapsulation disabled, you no longer have this option.

Finally, I want to add a few words about how to β€œstyle” components from libraries. If you need to change the default view for, say, all mat-selects in your application, it's often best to do so in global styles. Sometimes, some developers prefer to put these styles in a separate SCSS file and import it wherever needed, but in this case, when building the project, these styles are duplicated in each chank (compiled js file for a lazy or shared module / group of modules) , where at least one of the components included in this chank uses this style file, which can significantly increase the total size of the bundle. Therefore, this practice should be avoided.

Latest comments (1)

Collapse
 
ajitzero profile image
Ajit Panigrahi

Love the tip about using mixins for host! I repeat that often without considering this option