CSS variables serve as an exceptionally versatile mechanism for building Angular components—or components in any JavaScript framework—that are easy to customize, scale, and maintain.
Here, we’ll walk through a practical scenario that highlights how CSS variables can streamline the styling process for Angular UI components intended for reuse.
First, we’ll set up a component hierarchy that goes two levels deep:
@Component({
selector: 'child-level-2',
standalone: true,
template: `
<h2>Some Text</h2>
<p class="custom-paragraph">A big paragraph</p>
`,
styles: [
`
.custom-paragraph {
font-size: 20px;
color: blue;
}
`,
],
})
export class ChildLevel2Component {}
@Component({
selector: 'child-level-1',
standalone: true,
imports: [ChildLevel2Component],
template: ` <child-level-2></child-level-2> `,
})
export class ChildLevel1Component {}
@Component({
selector: 'parent',
standalone: true,
imports: [ChildLevel1Component],
template: ` <child-level-1></child-level-1> `,
})
export class ParentComponent {}
In this setup, a ParentComponent renders a ChildLevel1Component, which itself contains a ChildLevel2Component. Our goal is to adjust the font and color of a paragraph inside ChildLevel2Component, with those values originating from ParentComponent.
This scenario is quite typical across many applications, where we need to modify the styling of a deeply nested component in the hierarchy.
Let’s look at a few standard techniques and the drawbacks associated with each one.
@Inputs()
The most straightforward method for passing styles is via @Input().
@Component({
selector: 'child-level-2',
standalone: true,
template: `
<h2>Some Text</h2>
<p style="color: {{ color }}; font-size: {{ font }}px">A big paragraph</p>
`
})
export class ChildLevel2Component {
@Input() font = 20;
@Input() color = 'blue';
}
@Component({
selector: 'child-level-1',
standalone: true,
imports: [ChildLevel2Component],
template: ` <child-level-2 [color]="color" [font]="font"></child-level-2> `,
})
export class ChildLevel1Component {
@Input() font = 25;
@Input() color = 'orange';
}
@Component({
selector: 'parent',
standalone: true,
imports: [ChildLevel1Component],
template: ` <child-level-1 color="red" [font]="30"></child-level-1> `,
})
export class ParentComponent {}
The problem:
- Each style customization requires its own dedicated
@Inputs()property. - What’s worse, with a deeply nested component hierarchy this gets out of hand quickly — identical inputs must be threaded through every level of the tree.
- For third-party components we don't control, customization is impossible unless the author already exposed
@Inputsfor those specific attributes. - From my point of view, it's cleaner to keep the component's logic separate from its visual presentation.
ng-deep
Another frequent approach for styling overrides is ::ng-deep.
@Component({
selector: 'child-level-2',
standalone: true,
template: `
<h2>Some Text</h2>
<p class="custom-paragraph">A big paragraph</p>
`,
styles: [
`
.custom-paragraph {
font-size: 20px;
color: blue;
}
`,
],
})
export class ChildLevel2Component {}
@Component({
selector: 'child-level-1',
standalone: true,
imports: [ChildLevel2Component],
template: ` <child-level-2></child-level-2> `,
})
export class ChildLevel1Component {}
@Component({
selector: 'parent',
standalone: true,
imports: [ChildLevel1Component],
template: ` <child-level-1></child-level-1> `,
styles: [
`
:host child-level-1 ::ng-deep .custom-paragraph {
font-size: 30px;
color: red;
}
`,
],
})
export class ParentComponent {}
On the surface, this accomplishes exactly what we need. ::ng-deep lets us get around the style encapsulation that Angular and its Shadow DOM enforce, yet it is not recommended and warrants caution.
The reason ::ng-deep gets a bad rap is that it defeats the purpose of Angular's style encapsulation. This encapsulation is a core feature of Angular components, as it stops styles from one component from leaking into others. When you rely on ::ng-deep, you can target elements lying outside the component's own template, which can lead to unforeseen side effects and complicates both comprehension and upkeep of the app's styling logic.
A note in passing: For third-party UI libraries lacking alternative customization options, the
::ng-deeppseudo-class is, as of now, the sole means to style their components.
ViewEncapsulation
Angular, out of the box, gives every component an Emulated Shadow DOM. With styles or styleUrls, any CSS properties you define remain scoped to that very component and cannot be shared.
Still, you can switch the encapsulation mode to ViewEncapsulation.NONE. This makes every style you declare within that component applicable to any HTML element across the entire application.
@Component({
selector: 'child-level-2',
standalone: true,
template: `
<h2>Some Text</h2>
<p class="custom-paragraph">A big paragraph</p>
`,
styles: [
`
.custom-paragraph {
font-size: 20px;
color: blue;
}
`,
],
})
export class ChildLevel2Component {}
@Component({
selector: 'child-level-1',
standalone: true,
imports: [ChildLevel2Component],
template: ` <child-level-2></child-level-2> `,
})
export class ChildLevel1Component {}
@Component({
selector: 'parent',
standalone: true,
imports: [ChildLevel1Component],
template: ` <child-level-1></child-level-1>`,
encapsulation: ViewEncapsulation.None, // 👈
styles: [
`
.custom-paragraph { /* 👈 we override the css class */
font-size: 30px !important;
color: red !important;
}
`,
],
})
export class ParentComponent {}
- In my view, this approach poses a significant risk. I’d rather keep all global styling within
style.scssat the application’s root folder. Embedding global styles directly in a component can produce unforeseen side effects and may disrupt components that already exist. Only use it if you’re fully aware of the implications, and ensure your CSS class selectors are highly unique to avoid collisions.
Css variables
Css variables operate much like Angular’s @Input() decorator, except they apply to styling. They give us total command over what is modifiable, extending beyond the boundaries of a component’s encapsulation.
Here is the syntax for defining Css variables:
.custom-class {
--my-custom-font: 20px;
}
And used like the following:
font-size: var(--my-custom-font, /*default value*/)
/* ex: */
font-size: var(--my-custom-font, 10px)
font-size: var(--my-custom-font, var(--my-global-font, 5px))
/* invalid */
font-size: var(--my-custom-font, --my-global-font)
Now let's use it in our scenario, producing:
@Component({
selector: 'child-level-2',
standalone: true,
template: `
<h2>Some Text</h2>
<p class="custom-paragraph">A big paragraph</p>
`,
styles: [
`
.custom-paragraph {
font-size: var(--child-level-2-font-size, 20px);
color: var(--child-level-2-color, blue);
}
`,
],
})
export class ChildLevel2Component {}
@Component({
selector: 'child-level-1',
standalone: true,
imports: [ChildLevel2Component],
template: ` <child-level-2></child-level-2> `,
})
export class ChildLevel1Component {}
@Component({
selector: 'parent',
standalone: true,
imports: [ChildLevel1Component],
template: ` <child-level-1></child-level-1> `,
styles: [
`
:host child-level-1 {
--child-level-2-font-size: 30px;
--child-level-2-color: red;
}
`,
],
})
export class ParentComponent {}
The customization of our application is now fully in our hands, letting us sidestep Angular’s style encapsulation entirely.
One limitation, though, remains. When we attempt to establish a default color within ChildLevel1Component via a CSS variable, overriding that default from ParentComponent proves impossible. CSS variables follow the principle that the most recently declared value takes precedence.
Overcoming this requires defining a fresh CSS variable—one that the component hosting ChildLevel1Component can reference.
:host child-level-2 {
--child-level-2-font: var(--child-level-1-font, 10px);
--child-level-2-color: var(--child-level-1-color, yellow);
}
This brings us to the end of this article! At this point, you should have a solid grasp of how to master and apply styles to any component with ease in Angular (or any other JS framework).
I trust that you've picked up a new Angular insight along the way. Should you enjoy the content, feel free to connect with me on Twitter or Github.
👉 For those looking to speed up their Angular and Nx learning, be sure to explore Angular challenges.
