Skip to main content
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Andrew Duvall
Ben Nadel at NCDevCon 2011 (Raleigh, NC) with: Andrew Duvall

Solved: CSS Specificity And Shadow DOM Overrides In Angular 2.4.1

By on

Yesterday, I was struggling to overcome a CSS specificity issue in Angular 2's simulated shadow DOM functionality. After walking away from the problem, however, I realized that the issue was not in the shadow DOM emulation itself but rather in how I was applying CSS to my Angular 2 application. I was mixing metaphors, so to speak, using both external stylesheets and simulated shadow DOM. These two concepts don't play together very well. And, by moving external stylesheets into the shadow DOM of my root component, CSS specificity problems disappear.

Run this demo in my JavaScript Demos project on GitHub.

To quickly recap the issue I experienced yesterday, I was trying to define a global override for a default style that a given Angular 2 component was providing for its host element. My override was defined in an external stylesheet while the component's default style was defined in its own shadow DOM styles. Due to the fact that Angular 2 scopes shadow DOM styles using attribute selectors, my global override had a lower CSS specificity than the shadow DOM styles:

info-box { ... } < [ _nghost-blam-1 ] { ... }

Since the Type selector (info-box) of my global override has a lower specificity than the Attribute selector ([_nghost-blam-1]) of the target component, my global override was never applied.

One possible way to fix this would be to add an Attribute selector to my global override. And, the fun thing about CSS is that this attribute selector doesn't have to be part of the info-box element - it just has be somewhere in the selector. Such as another component's simulated shadow DOM attribute selector.

By moving the global stylesheet into the shadow DOM of the root component, that's exactly what we get. When we move our global styles into the shadow DOM of the root component, all of our global style selectors are scoped to the attribute selector of the root component's shadow DOM. This essentially normalizes specificity across all shadow DOM emulations, allowing you to think, once again, about traditional CSS relationships.

To see this in action, let's look at the info-box component from yesterday's post. The code here hasn't changed at all:

// Import the core angular services.
import { ChangeDetectionStrategy } from "@angular/core";
import { Component } from "@angular/core";

@Component({
	selector: "info-box",
	inputs: [ "avatarUrl", "name", "title" ],
	changeDetection: ChangeDetectionStrategy.OnPush,
	styles: [
		`
			:host {
				border: 1px solid #CCCCCC ;
				border-radius: 4px 4px 4px 4px ;
				box-sizing: border-box ;
				display: table ;
				margin: 0px 0px 0px 0px ; /* This is the property to be overridden. */
				min-width: 100px ;
				padding: 20px 27px 20px 27px ;
				text-align: center ;
			}

			.avatar {
				display: block ;
				border-radius: 50% ;
				height: 75px ;
				margin: 0px auto 0px auto ;
				width: 75px ;
			}

			.name {
				font-size: 22px ;
				line-height: 24px ;
				margin: 18px 0px 5px 0px ;
			}

			.title {
				color: #999999 ;
				font-size: 16px ;
				line-height: 18px ;
			}

			/* -- variations - host class targeting. -- */

			:host( .mini ) {
				margin: 10px 0px 10px 0px ;
			}

			:host( .mini ) .name,
			:host( .mini ) .title {
				display: none ;
			}

			/* -- variations - media query targeting. -- */

			@media screen and ( max-width: 600px ) {
				:host {
					border: none ;
					height: 75px ;
					min-width: 75px ;
					padding: 0px 0px 0px 0px ;
					width: 75px ;
				}

				.name,
				.title {
					display: none ;
				}
			}
		`
	],
	template:
	`
		<img [src]="avatarUrl" class="avatar" />

		<div class="name">
			{{ name }}
		</div>

		<div class="title">
			{{ title }}
		</div>
	`
})
export class InfoBoxComponent {

	public avatarUrl: string;
	public name: string;
	public title: string;


	// I initialize the component.
	constructor() {

		this.avatarUrl = "";
		this.name = "";
		this.title = "";

	}

}

In this particular exploration, we're going to be overriding the host element's "margin" property. Notice that margin is defined on both ":host" and on ":host(.mini)". We're going to be overriding both of those use-cases from our root component. And, since we're moving our global stylesheet into the shadow DOM of the root component, the root component is the only other piece of code that we need to look at:

// Import the core angular services.
import { Component } from "@angular/core";

@Component({
	selector: "my-app",
	styles: [
		`
			:host {
				display: block ;
			}

			/*
				By using the "deep" operator ( >>> or /deep/ ), we can provide override
				styles for any component consumed within this application component tree.
				Since we are deep-scoping this to the :host element, Angular will use an
				attribute selector followed by the type selector:
				--
				[ _nghost-blam-1 ] info-box { ...overrides... }
				--
				... which will be able to override default host styles provided in the
				info-box component itself since the info-box component will only be using
				an attribute selector.
				--
				Attribute + Type > Attribute
				--
				As such, the CSS selector below will have a higher specificity.
			*/
			:host >>> info-box {
				margin: 16px 0px 16px 0px ;
			}

			/*
				We can also override a specific instance of the info-box host element.
				Since this is an element inside the current component's shadow-DOM, it
				will be given an attribute selector:
				--
				info-box.mini[ _ngcontent-blam-1 ] { ...overrides... }
				--
				... which will be able to override default host styles provided in the
				info-box component itself since the info-box component will only be using
				an attribute selector.
				--
				Type + Class + Attribute > Attribute
				--
				As such, the CSS selector below will have a higher specificity than both
				the deep-scoping overrides above and the default info-box styles.
			*/
			info-box.mini {
				margin: 8px 0px 8px 0px ;
			}
		`
	],
	template:
	`
		<info-box
			avatarUrl="./sarah.png"
			name="Sarah Connor"
			title="Freedom Fighter">
		</info-box>

		<info-box
			avatarUrl="./sarah.png"
			name="Sarah Connor"
			title="Freedom Fighter"
			class="mini">
		</info-box>
	`
})
export class AppComponent {
	// ...
}

In the first override, by using the "deep" selector (>>>), we're telling Angular not to scope the info-box token itself such that this CSS rule will be applied to any info-box element anywhere in the component tree. However, since this selector is still part of the root component's shadow DOM, Angular will prefix the selector with the root component's attribute selector:

CSS specificity in Angular 2 shadow dom.

Now, our global override selector contains both an Attribute selector and a Type selector, which becomes more specific than the Type selector in the info-box shadow DOM:

[ _nghost-dlt-0 ] info-box { ... } > [ _nghost-blam-1 ] { ... }

As such, when we look at the CSS that is being applied to the info-box instance on the page, we can see that our global style is finally overriding the default style of the info-box host element:

CSS specificity in Angular 2 shadow dom.

As you can see, the default "margin" style of the info-box component is being overridden. This is because the attribute selector in the root component's shadow DOM essentially cancels out the specificity of the attribute selector in the info-box component's shadow DOM. After this is normalized, the global override's Type selector gives the global override a higher specificity than the info-box component's host styles.

CSS specificity in Angular 2 shadow dom.

In this demo, I'm also targeting "info-box.mini" to demonstrate that we don't have to use the "deep" operator to leverage the normalization of specificity. And, if we look at the CSS applied to the second info-box instance, we can see that the non-deep targeting of info-box in the root component's template works just as we would expect it to:

CSS specificity in Angular 2 shadow dom.

After many years of using external CSS stylesheets, it's hard to start thinking about the root component of an Angular 2 application in terms of shadow DOM. But the reality is, your entire application is encapsulated within the shadow DOM of the root component. As such, it makes sense for the global styles of your application to be part of the root component's shadow DOM styles. Luckily, thinking this way normalizes CSS specificity across the component tree and makes it possible to override a component's host styles.

Want to use code from this post? Check out the license.

Reader Comments

1 Comments

Hey Ben, great article! FWIW, if one is using SASS with Angular (ie. with Angular CLI) using `>>>` does not work at present, therefore, one must still use `/deep/`.

15,688 Comments

@All,

This morning I sanity-checked the behavior of "styleUrls" in the Angular component meta-data. Turns out, I had been making a very poor assumption - that shared styleUrls would create duplication in the compiled assets. This is, in fact, not true:

www.bennadel.com/blog/3372-sanity-check-shared-style-urls-are-only-compiled-into-angular-5-0-1-once.htm

... the shared styleUrl just gets compiled as a single module and the required into each consuming component (at least in the way I am compiling with Webpack). This is very exciting because it means that much of what would have been higher-up in the component tree can actually be moved down into various components where it becomes much more clear and locally-scoped from a view-encapsulation standpoint.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel