In this article we will discuss and learn in detail about Angular styling mechanism which works underneath the layer, and how it isolates the styles of the component.
And Mostly we will focus on learning how to override the CSS properties of third party components in Angular. We will also talk about the advantages of component style isolation and how to debug styles if something is not working as expected.
Keywords: ':host', ':host-context', '::ng-deep'
When we have hard timeline and want our apps in market as soon as possible it's better to use third party libraries (like Kendo, ionic etc.) which has reusable components, this speeds up the application development. However, most of the time we need to customize the components so that it meets the company theme color and keeps the UX designers happy. To override the CSS, we need to figure out the particular CSS selector, it can be class, id, element or any other context from the third party component and override the properties. In this blog post, I would like to explain and cover how to accomplish the goal in the angular Application.
First let's discuss about Style isolation for the Angular components and Why it's an important aspect to isolate the styles?
One key reason is it's easy to maintain and I feel that itself is sufficient and strong reason to isolate the styles, there shouldn't be any doubt about it.
Other reason what I think could be, As we keep developing multiple components for a bigger applications, we tend to run into situations where styles from one feature (or component) start interfering with the styles of another. I think this you should be able to relate most of the time.
This is mainly because most browsers do not have widespread support for style isolation yet, where we can limit (or constrain) one style to be applied only to a particular part of the page.
If we are not careful, we will quickly run into CSS maintenance issue and trust me it's very cumbersome and tedious effort to figure out the issue at run time and fix it.
What you think, wouldn't be great to just style our components not bothering in which scenarios it will override?
Some more Benefits of Style Isolation
When we use third party components (like Kendo, ionic etc. etc. ) and add it to our application , sometimes we just realize it's totally broken due to styling issues.
Style isolation would allow us to ship our components knowing that the styles of the component will (most likely) not be overridden by other styles in target applications. This makes the component effectively much more reusable, because the component will now in most cases simply just work, styles included.
Angular View Encapsulation brings us all of these advantages, so let's learn how it works!
Here we will see how styling works underneath the layer and this is the best way to learn and understand the whole mechanism and it will also unlock us to debug the styles if needed.
Let's assume we have multiple pages in our application and multiple custom components.
For simplicity Let's assume we have a custom button component, which has below CSS defined:
Where ever this component will be used, button will be visible with green color background.
Now Suppose, we have a app component, which contains a html button element with CSS class "green-button" and our custom button component. Below is the code structure.
If you run the application, we will have two buttons, one with the background color "red" and other with background color "green". Why is it so? Both are buttons and both have CSS class "green-button" then also CSS class of custom-button component is not overridden .
If you didn't know that there was some sort of style isolation mechanism in action. Let's see how this mechanism works, because knowing that is what is going to allow us to debug it, if needed.
To better visualize how default view encapsulation works(i.e. emulated), let's see what the above defined app-root custom HTML element will look like at runtime:
Let's understand few things which are happening at the runtime HTML i.e.
- a HOST property "_nghost-c0" is added to the app-root.
- each of the HTML elements (child elements) inside the application root component has content property "_ngcontent-c0"
- custom-button element has new host property "_nghost-c1".
- The custom-button element is still tagged with the _ngcontent-c0 property which is applied to all template elements on the application root component. But the elements inside the custom-button template get applied the _ngcontent-c1 property instead!
Let's then summarize how these special HTML properties work, and then see how they enable style isolation:
On application startup (or at build-time with AOT), each component will have a unique property attached to the host element, depending on the order in which the components are processed: _nghost-c0, _nghost-c1, etc.
Together with that, each element inside each component template will also get applied a property that is unique to that particular component: _ngcontent-c0, _ngcontent-c1, etc.
This is all transparent and done under the hood for us.
How view Encapsulation is Enabled with these(_nghost-c0,_ngcontent-c0, etc.) strange properties?
The presence of these properties could allow us to write manually CSS styles which are much more targeted than just the simple styles that we have on our template.
For example, if we want to scope the green color to the custom-button component only, we could write manually the following style:
While style 1 was applicable to any element with the green-button class anywhere on the page, style 2 will only work for elements that have that strangely named property!
So this means that style 2 is effectively scoped to only elements of the custom-button component template, and will not affect any other elements of the page.
So we now can see how those two special properties do enable some sort of style isolation, but it would be cumbersome to have to use those properties manually in our CSS (and in fact, we should not).
But luckily, we don't have to. Angular will do that automatically for us.
At startup time (or at build time if using AOT), Angular will see what styles are associated with which components, via the styles or styleUrls component properties.
Angular will then take those styles and apply them transparently the corresponding isolating properties, and will then inject the styles directly into the page header as a style tag:
The _ngcontent-c1 property is unique to elements of the custom-button template, so the style will be scoped to those elements only.
And that is how the Angular default view encapsulation mechanism works!
Overriding CSS properties of third-party angular components
Suppose we have a home-page component which has component inside it and we want to override the color of button.
However, it will not work due to the emulated view encapsulation which is the default mode for Angular components. The resulting CSS selector has the following form:
However, the element that you want to target using the CSS selector is not a child, but just a descendant of the home-page component. Therefore, the resulting selector, which takes into account the emulated view encapsulation, does not affect the desired HTML element.
Now you might be tempted to switch off the emulated view encapsulation for the home-page component in order to get the desired result. However, it will simply define a global CSS rule which affects all the components in our application. Whether it works or not depends on the order of CSS rules, namely if the default one comes first, the customized one takes precedence.
One solution is to make use of the ::ng-deep pseudo-class, which simply means that from now on you do not want the CSS selector to be affected by the emulated view encapsulation. However, it’s quite a common mistake to come up with the following CSS rule:
The resulting CSS selector is:
It results in a global CSS rule being created, since there is no context provided. When it comes to this rule, there is no difference as compared to turning off the emulated view encapsulation for the home-page component.
Luckily, you can provide a proper context to the CSS selector (telling you only want to affect the element which is a descendant of the home-page component) using the following CSS selector:
The resulting CSS selector is:
The context for the CSS rule is represented by the attribute ([unique-attr]) within the selector, which has been provided by the emulated view encapsulation mechanism!!!!
Since you do not reference a dynamic component, e.g. the tooltip component, using an element selector, you cannot apply a custom class/id/attribute like with the homepage component. As long as a third-party component is a descendant of your component in the DOM hierarchy, you can make use of the first presented solution:
However, some components allow you to choose a container to which their HTML root element will be appended to:
Now, the above CSS selector will not target the desired element, since it is not a descendant of the custom-tooltips component which is enforced by the :host part of the selector. Fortunately, the tooltip component’s author enabled providing a custom CSS class that will serve as a context for your CSS selector in the global styles:
As a result, you do not need to choose whether to take advantage of styles customization or append the element to a desired container other than the default one.
You can override a third-party component’s CSS properties either at an enclosing component or global level. Unless you want to affect all the third-party component instances, remember to provide a unique context to the CSS selector. Keep in mind that using ::ng-deep pseudo-class without any context defines a global CSS rule.
Dynamic components may be appended to a different HTML element than the one which triggers its mount, which affects how you can override CSS properties in such scenario.