Build a consistent Style API for Angular Components
Published: 2024-06-06 10:13:15

While creating a new component library for an ERP-System, we invested a bit of time to rethink how we want to make styling of components easier than before.

Of course, nearly every Component library offers a way of creating themes. Nevertheless, there are special requirements that afford a style-override that absolutely fits in the design, but is not part of the library at this point in time.
Possibly, you want to slightly adjust a colour or spacing.
Perhaps, you wish to be able to use another template for a list-item inside a component, but the Component's API does not allow you to.

## 🕺🏻 The “CSS Override Dance”

In order to solve our design-challenge, we used to…

1. … find the HTML-Element in the respective Component that requires styling.
1. … inspect the Element to determine which CSS class requires an override.
1. … come up with the correct CSS-override.

In the past, our team used the following ways to override Component Styles:

1. Adding another CSS class that can be appended to the Component's Host-Element itself
1. Overriding an existing CSS class by turning off _ViewEncapsulation_
1. Using [::ng-deep](https://angular.dev/guide/components/styling#ng-deep)
1. Playing with the [!important-Rule](https://www.w3schools.com/css/css_important.asp).
1. If provided, using a Hook of the Component library to override styles (e.g. [PrimeNG](https://primeng.org) provides the _input()/@Input()_ styleClass for lots of their components).
1. In exceptional cases, we have re-implemented the component for our needs. 🤫

To be honest, we are not happy working like this.

## The Problem

- Most of the time, we came up with a different approach for overriding styles that need to be explained to fellow team members
- We had to invest more time in finalizing a feature than expected due to the additional workload to customize a component.
- Once the Component library received a major update, our overrides do not work any more, due to breaking changes.
- Custom Style-APIs like PrimeNG's _styleClass_ help, but aren't consistently applied everywhere. Sometimes it works, sometimes not.

## The aim

- Instead of trying out different ways of customizing a Component, we want **one clear API**, that is easy to use for everybody in the team.
- This API should be consistent across all components.
- Ideally, we can stick with the web-standards.
- Provide a Style-API for our teammates, allowing them to customize our Components with ease.

## Inspiration

### QwikUi
> https://qwikui.com/

We started looking for alternative solutions. We were surprised to see, that style overrides are a very common and simple thing to do in Frameworks using [JSX](https://react.dev/learn/writing-markup-with-jsx).

```tsx
export const Label = component$<LabelProps>((props) => {
  return (
    <HeadlessLabel
      {...props}
      class={cn(
        'font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
        props.class,
      )}
    >
      <Slot />
    </HeadlessLabel>
  );
});
```
[Source: Qwik UI](https://github.com/qwikifiers/qwik-ui/blob/13e0d53a7b0debceb2e41a01dcca02bb6aca0b61/packages/kit-styled/src/components/label/label.tsx#L11)

The snippet demonstrates the class-overrides can simply be passed down the Component-Tree. Provided CSS classes can be merged dynamically based on certain conditions.

💫 Our team was excited by the fact that styling can be done using Web-Standards without workarounds.

### Angular Material
> https://material.angular.io/

Now, it was our turn to try if we can come up with a solution in Angular. We were wondering if we can come up with a clean solution by manipulating the _class_-Attribute of the Component's host element. In our experience, this approach is not widely used, but we remembered that we have seen this in _Angular Material_ components.
That's why we started looking at the implementation and gained confidence to go with the Host-Binding-Approach. Nearly every component in Angular Material specifies host-classes (see [MatFabButton](https://github.com/angular/components/blob/main/src/material/button/fab.ts#L66)).

## Our consistent Component-Style-API

We do not consider to have found the silver bullet yet. We just want to share our findings with you and are **happy to receive feedback** in order to check if we are on the right path or if we might consider other ways.

### Style-API Rules

1. We provide basic styling, to allow the team to use the Components right away.
1. Every developer can customize each Component using the `class`-Attribute.
1. We agree on a standardized way of applying CSS classes to be able to optimize them, behind the scenes.

### 👩🏻‍💻 Let's code
> You will find the full working example [on GitHub](https://github.com/GregOnNet/ng-consitant-component-style-api/blob/consistant-component-style-api-1/src/app/label.component.ts) here.

We start simple with a Component representing a Label.
By default, the label has a gray text. If needed, the text-color can be overridden.

> By the way, we use [TailwindCSS](https://tailwindcss.com/) to have a standardized way of applying CSS classes

![A gray and a green label next to each other](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vnra2ovro7y15g37i4ba.png)

Let's start by having a look at the usage.
You will see it is like working with standardized HTML-Elements.

```html
<!-- Component Template -->
<app-label>Default</app-label>
<app-label class="text-green-500">Green</app-label>
```

The _class_-Attribute is applied to the Component-Host-Element.
Next, we will dive into the implementation of the _LabelComponent_.

```ts
import { Component, computed, input } from '@angular/core';
import { twMerge } from 'tailwind-merge';

@Component({
  selector: 'app-label',
  standalone: true,
  host: { '[class]': 'hostClass()' },
  template: `<ng-content></ng-content>`
})
export class LabelComponent {
  #classDefaults = 'p-2 text-slate-600';

  class = input<string>('');

  protected hostClass = computed(() => {
    const classOverrides = this.class();

    return twMerge(this.#classDefaults, classOverrides);
  });
}
```

- In the _Component-Decorator_, we bind a [computed signal](https://angular.dev/guide/signals#computed-signal-dependencies-are-dynamic) to the class attribute.
- In the Component-Class, we add the _input()_ `class` to allow style overrides.
- The computed _hostClass()_ allows us to merge the `#classDefaults` with the `classOverrides`.

> In _JSX_ we achieve the same by writing `class={`p-2 ${...props.class}`}`.

The package [tailwind-merge](https://github.com/dcastil/tailwind-merge) helps us to combine the tailwind classes, safely. It will replace the defaults once an override is provided. For example, _text-slate-600_, will be removed as soon as _text-green-500_ has been provided. 

![HTML Code showing two labels and which CSS has been applied](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mbkjjn6x8ls3wqkfeful.png)

## The simpler way

We found an open issue in the Angular repository that would simplify the whole styling process. 

Eventually, we will get the possibility to manipulate the Component's host-Element in the template: [see \<ng-host>](https://github.com/angular/angular/issues/19119)

## Summary

We encountered difficulties customizing components to fit certain design requirements. We got inspired by different Frameworks:
- [Angular Material](https://material.angular.io/)
- [PrimeNg](https://primeng.org/)
- [QwikUI](https://qwikui.com/)
- [TaigaUI](https://taiga-ui.dev/) 
- [SpartenUI](https://www.spartan.ng/).

> 🤝 We decided to provide a Style API that is as close as possible to existing Web-Standards. These are well known by web developers or can be easily communicated.

## 🙏🏻 Special Thanks

First, I want to thank you [Melory](https://www.linkedin.com/in/melory-ayala/) for going through all the discussions with me to find a good approach for our Component library. You know, I appreciate your input and our constructive & productive discussions.

I also thank all the community members for reviewing this article. I appreciated chatting with you all. Furthermore, I received valuable feedback from you.

- [@ArthurGroupp](https://x.com/ArthurGroupp)
- [@GeromeDEV](https://x.com/GeromeDEV)
- [@kuncevic](https://x.com/kuncevic)

Rock on & Code
Gregor