Angular Styling Made Easy: Leveraging the Power of CSS Variables

achtlos

thomas

Posted on January 10, 2023

Angular Styling Made Easy: Leveraging the Power of CSS Variables

Css variables are a very powerful tool for creating highly customizable, scalable, and maintainable components in Angular or any other JavaScript framework.

In this example, we will demonstrate how to use CSS variables to simplify style customization for shareable UI components in Angular.

To begin, let's create a component tree with a depth of 2:

@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 {}
Enter fullscreen mode Exit fullscreen mode

In this example, we have a ParentComponent that calls a ChildLevel1Component which in turn has a ChildLevel2Component . We want to customize the font and color of a paragraph within ChildLevel2Component from ParentComponent.

This is a common pattern in many applications. We often want to alter the appearance of a deep component in the component tree.

We will examine some common solutions and the pitfalls of each approach.

@Inputs()

The naive way to pass desired styles would be though @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 {}
Enter fullscreen mode Exit fullscreen mode

Issue: 

  • We will need to add an @Inputs() for each new property we want to customize.
  • Worst, this quickly becomes unwieldy if we have a deeply nester component structure, as we would need to add the same inputs to every component down the tree. 
  • If we want to customize a component from a third party library that we don't own, it won't be possible if the library's author has not provided @Inputs for the desired properties. 
  • Personally, I prefer to separate the functional aspect of the code from the appearance of a component.

ng-deep

The next common way to customize style 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 {}
Enter fullscreen mode Exit fullscreen mode

At first sight, this is doing exactly what we want. ::ng-deep allows us to bypass the style encapsulation provided by Angular and its Shadow DOM. However ::ng-deep is not recommended and should be used carefully.

The main reason ::ng-deep is considered bad is because it undermines the purpose of style encapsulation in Angular. Style encapsulation is an important feature of Angular components because it helps to ensure that styles defined in one component do not affect the styles of other components. By using ::ng-deep, you can apply styles to elements that are not in the template of the component, which can potentially cause unexpected side effects and make it more difficult to understand and maintain the styles in your application.

Side note: Currently, ::ng-deep pseudo-class is the only way to customize UI components in a third party library that has not provided other customization methods.

ViewEncapsulation

By default, Angular creates an Emulated Shadow DOM for each component. All Css properties declared within a component via styles or styleUrls are local to that component and are not shareable.

However we can change the encapsulation of our component to ViewEncapsulation.NONE, which means that all styles declared in that component can be applied to any other HTML elements within 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 {}
Enter fullscreen mode Exit fullscreen mode
  • I find this option very dangerous. I prefer to keep my global styles inside the style.scss file at the root of my application. Defining global styles inside a component can cause unexpected behavior and can break already existing components. Use this with extreme cautious and only if you know what your are doing. Be very specific with your CSS class names. 

Css variables

Css variables are like Angular's @Input() decorator, but for styles. They allow us to keep full control over what can be changed and can be extended outside the scope of component's encapsulation.

Css variables are defined in the following way:

.custom-class {
  --my-custom-font: 20px;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Let's apply it to our example, which gives:

@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 {}
Enter fullscreen mode Exit fullscreen mode

That's it, we can customize our application as we please and bypass the style encapsulation of Angular.

However there is still one caveat. If we want to set a default color inside our ChildLevel1Component using a CSS variable, we won't be able to override it inside ParentComponent. The rule for CSS variables is that the last one defined will be applied.

The solution is to create a new CSS variable that the parent component of ChildLevel1Component can use.

:host child-level-2 {
  --child-level-2-font: var(--child-level-1-font, 10px);
  --child-level-2-color: var(--child-level-1-color, yellow);
}
Enter fullscreen mode Exit fullscreen mode

That's it for this article! You should now master and styles any component easily in Angular (or any other JS framework).

I hope you learned new Angular concept. If you liked it, you can find me on Twitter or Github.

👉 If you want to accelerate your Angular and Nx learning journey, come and check out Angular challenges.

💖 💪 🙅 🚩
achtlos
thomas

Posted on January 10, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related