DEV Community

Cover image for Build a consistent Style API for Angular Components
Gregor Woiwode
Gregor Woiwode

Posted on

Build a consistent Style API for Angular Components

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.
  2. … inspect the Element to determine which CSS class requires an override.
  3. … 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
  2. Overriding an existing CSS class by turning off ViewEncapsulation
  3. Using ::ng-deep
  4. Playing with the !important-Rule.
  5. If provided, using a Hook of the Component library to override styles (e.g. PrimeNG provides the input()/@Input() styleClass for lots of their components).
  6. 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.

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

Source: Qwik UI

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).

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.
  2. Every developer can customize each Component using the class-Attribute.
  3. 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 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 to have a standardized way of applying CSS classes

A gray and a green label next to each other

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

<!-- Component Template -->
<app-label>Default</app-label>
<app-label class="text-green-500">Green</app-label>
Enter fullscreen mode Exit fullscreen mode

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

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);
  });
}
Enter fullscreen mode Exit fullscreen mode
  • In the Component-Decorator, we bind a computed signal 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 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

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>

Summary

We encountered difficulties customizing components to fit certain design requirements. We got inspired by different Frameworks:

🤝 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 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.

Rock on & Code
Gregor

Top comments (0)