CSS Custom Props - Override Angular Styles Like A Pro

Design is how we breathe here at SpotDraft! Every pixel rendered on your screen is carefully crafted and reviewed. Providing the user an intuitive sense of the application has a lot to with the design and when you go down that path on the web, CSS is your best friend!

CSS has gone under major improvements over the years. One of the recent and path breaking changes was the introduction of CSS variables, aka custom properties. It targets to solve the problem of maintaining common/repetitive styles by providing variables that can be scoped locally or globally. You define your variables once and you can reuse it in other components by CSS import.

The Problem of Overriding Styles

One of the recent challenges we came across in the implementation of styling is to override styles. View encapsulation makes it easy to contain styles but at the same time gives hard time overriding styles. You might have to break encapsulation to get around this and that’s a big NO!

It gets even dirtier when you want to override CSS styles of a child component. Even component libraries like Material components don't provide a reliable alternative to do this. One way to go about it is to dirty the global styles.scss with component specific overrides which would look something like,


app-random-component {
  .ng-select {
    background: green;
  }
}

But we all know polluting global styles.scss isn’t the ideal solution.

CSS Variables to the Rescue

So, we came up with this convention for custom components to declare variables using :host selector. Let’s take an example of a custom button component. Let’s assume that we need to have the background color of the button to be overridden from the parent components. 

Declare the variable in :host selector,


:host {
 	--app-button--background: blue;
} 

This will scope the variable to the component. Then apply this style to the button element like,


.btn {
	background: var(--app-button--background)
}

Then in the parent component where app-button component is being used (say app-card), you can simply override the variable,


.is-rejected {
  app-button {
    --app-button--background: red;
  }
}

Such a neat and clean solution!

The beauty of this approach is that it is pure CSS. That means this approach will seamlessly work with cascading and states like  :hover :focus etc. This approach also makes it possible to override global styles and keep component specific styles inside the component itself.

Taking it Forward

Use of custom variables to override styles made our library implementation a lot easier. We didn’t stop there. We then built out an Angular directive to help us with overriding custom props. 


@Directive({
  selector: "[sdkApplyCssProps]"
})
export class ApplyCssPropsDirective implements OnChanges, OnInit {
  @Input("sdkApplyCssProps")
  customProps: Record<string, string> = {};

  constructor(private el: ElementRef) {}
  ngOnInit() {
    this._setProps();
  }
  ngOnChanges(changes: SimpleChanges) {
    this._setProps();
  }

  private _setProps() {
    const nativeElement: HTMLElement = this.el.nativeElement;
    if (nativeElement) {
      Object.getOwnPropertyNames(this.customProps).forEach(prop => {
        if (prop.startsWith("--")) {
          nativeElement.style.setProperty(prop, this.customProps[prop]);
        } 
       });
    }
  }
}

The directive simply takes an Input property, i.e. customProps, which is an object with the list of props that we need to override. We then override these props using  the private method _setProps(). This makes it easy to pass props to any child component from the parent through controller. This way, dynamic styling is now super easy and intuitive to the screen reader.

An example of using the directive would be


@Component({
 selector: "app-card",
 template: `
   <app-button
       [sdkApplyCssProps]="getCustomStyle()"
   >Click</app-button>
 `,
})

export class CardComponent {
 getCustomStyle(){
   return {
     "--app-button--background": options.backgroundColor,
     "--app-button-color": options.fontColor,
   };
 }
}

Elegant, ain’t it?