Handle parent modifier classes in Angular

The different ways of listening to a parent modifier class or host context class in Angular’s scoped CSS.

Gav McKenzie
Author
Gav McKenzie
Published
Feb 22, 2022
Topics
Industry

I've been converting a bunch of components over from a legacy Gulp build that generates a global stylesheet to use the built in Angular scoped CSS.

There’s been a few gotchas along the way but this is a good one.

We use a lot of BEM CSS selectors at Etch, specifically using modifier classes on the parent (or Host) element to create different variations of a component.

Angular’s scoped CSS has two main contexts. The host (:host) element and the children (content). The host element is the top level parent element for the component that the Angular @Component or directive is attached to. HTML elements that can be seen in the template property are the content elements.

When you write the CSS, you have to sepcifically target either the host or the CSS will only apply to content selectors.

<adm-btn-call _nghost-idg-c676 class="adm-btn-call">
<button type="button" _ngcontent-idg-c676 class="adm-btn-call__btn">
<span _ngcontent-idg-c676 class="adm-btn-call__text">
Call us
</span>
</button>
</adm-btn-ball>

This simple example component shows us how Angular designates host vs content. The host element has an property added to it of _nghost-idg-c676, whilst the child elements have the property _ngcontent-idg-c676.

In the CSS, we can use the :host pseudoselector to target the host.

// Compiles to [_nghost-idg-c676].adm-btn-call
:host.adm-btn-call {
background: rebeccapurple;
}
// Compiles to [_ngcontent-idg-c676].adm-btn-call__btn
.adm-btn-call__btn {
color: white;
}

We can add a modifier to this component to change the appearance of the UI.

<adm-btn-call _nghost-idg-c676 class="adm-btn-call adm-btn-call--invert">
<button type="button" _ngcontent-idg-c676 class="adm-btn-call__btn">
<span _ngcontent-idg-c676 class="adm-btn-call__text">
Call us
</span>
</button>
</adm-btn-ball>

And update the CSS to add the new styles. We're using :host-context on the child to attach the CSS from the parent selector.

// Compiles to [_nghost-idg-c676].adm-btn-call--invert
:host.adm-btn-call--invert {
background: white;
}
// Compiles to
// [_nghost-idg-c676].adm-btn-call--invert .adm-btn-call__btn[_ngcontent-idg-c676],
// .adm-btn-call--invert [_nghost-idg-c676] .adm-btn-call__btn[_ngcontent-idg-c676],
.adm-btn-call__btn {
[...]
:host-context(.adm-btn-call--invert) & {
color: rebeccapurple;
}
}

That juicy child selector means the modifier class can be on the host or an unknown ancestor of the host that the host sits within and then the child within that. Bit odd, but OK.

Now, say we decide to split the button out into its own subcomponent with its own @component decorator.

<adm-btn-call _nghost-idg-c676 class="adm-btn-call adm-btn-call--invert">
<button type="button" _nghost-idg-c677 class="adm-btn-call__btn">
<span _ngcontent-idg-c677 class="adm-btn-call__text">
Call us
</span>
</button>
</adm-btn-ball>

Notice the button is now a host.

We move the CSS for the button into it’s own file.

// Compiles to
// [_nghost-idg-c677].adm-btn-call--invert .adm-btn-call__btn[_nghost-idg-c677],
// .adm-btn-call--invert [_nghost-idg-c677] .adm-btn-call__btn[_nghost-idg-c677],
.adm-btn-call__btn:host {
[...]
:host-context(.adm-btn-call--invert) & {
color: rebeccapurple;
}
}

Now the selectors are broken because the selector is expecting the modifier to be on the host for this subcomponent, not its parent.

Here’s the magic

To target the :host element of a subcomponent based on a modifier attached to the parent component you have to go back to more normal SCSS and just do...

// Compiles to
// .adm-btn-call--invert .adm-btn-call__btn[_nghost-idg-c677]
.adm-btn-call__btn:host {
[...]
.adm-btn-call--invert & {
color: rebeccapurple;
}
}

It appears Angular doesn’t scope the modifier selector and everything works as expected.

TL;DR

You can always use :host-context to target content/child components based on poarent modifiers because of the way Angular spits out the CSS selectors.

If you want to target a host element based on a parent selector, you need to fall back to standard SCSS parent selector styles.