How to build a scalable Angular design system

Sergey Gultyayev
5 min readNov 6, 2021

Found yourself in a project with 10 identical primary buttons? Or starting a new project and want to ensure an easy process of development by reusing simple primitives?
Read further and I’ll share with you the tips on how to keep your projects well organized and avoid common pitfalls that you are most likely seeing in your project right now.

Code snippet showing two buttons. One with lots of class names styling it. Another one is identical to the first one, but with just one attribute.

TL;DR;

Introduce a UI documentation tool.
Make sure it’s always up to date and the team is actively using at or at least is aware of.
Embrace the power of atomic components which use attribute selector.

Single source of truth

First of all, we need someplace to document all the primitives and their combinations we have.

The most popular solution is Storybook. It’s easy to add to an existing project, can be used with any framework. This is where we will collect our primitives and refer to when onboarding new developers.

You may use any other alternative but the key idea is to have all available components documented and listed in one place.

Building the design system

Most likely that you like me know how to style components using classes. And when needed to create a design system for a project would mostly rely on some global classes and their combinations. That’s how the UI libraries are built.

When following this naive approach you would end up with a structure similar to this

styles
- _buttons.scss
- _controls.scss
...

And would be writing the markup in the following manner

<input type="text" class="control">
<button class="btn primary">Submit</button>

Then you would need some variations and the button would have a class name like this btn primary large . See where we are going? The more variabilities we have the more classes we need. Furthermore, most likely you would have sets of such classes which you need to repeat over and over.

This becomes hard to read, you need a reference to copy this button from as you cannot remember all these classes. And after some time designers come (they always do) and they change some styles. Now your old classes and their usages are invalid and you need to go over all places and update classes on each button.

Is there anything we could do to help ourselves? And the answer is yes, components with attribute selectors and directives come to the rescue!

Remember, we are working in Angular? This framework allows us to build our SPAs using primitives of any size and complexity and thus it becomes easier to be tempted to make one huge component containing all. However, if we look at our framework’s competitor React, people using it more often build very basic components like Button, Card etc and then use them. Why don’t we?

Let’s use this approach as well. We can build our UI primitives and their styles are encapsulated and kept close to the elements they style, so we won’t be ever wandering across the project looking if a class is being ever used.

Component and Directives

Now, you may think that I went nuts and say “The directives? They don’t have styles! You should be using components!”. Not quite, let me explain why and show you the power the directives could give you.

First, let’s start with the components. When one thinks of a component a similar structure comes to mind

@Component({
selector: 'my-button',
template: '<button>{{name}}</button>'
})
export class MyButton {
@Input() name: string;
}

And be used in a template using the following syntax
<my-button name="Submit"></my-button>

However, I would strongly advise against this kind of implementation, here's why.

Let’s say you need to add a new attribute to the button. You would need to add a new input for that. Then if you need one more you would need one more attribute and it would be an ever-growing list of inputs as the native button can have lots of useful attributes which we don’t need now, but may need in the future.

Instead, we could use an attribute selector based approach and the button would look now like this:

@Component({
selector: 'button[my-button]',
template: '<ng-content></ng-content>'
})
export class MyButton {}

Now the usage looks like this <button my-button>Submit</button>.

After that, we don’t need to modify our component every time we need to pass a new attribute to the underlying button as we don’t hide it! This way we even achieved the open-closed principle as we have greatly reduced the number of possible upcoming changes to the component we created.

Notice here the ng-content tag, we need this so we don’t lose the button’s content. Also, thanks to the new approach we could add an icon to our button with only a few style changes if needed and no new inputs to the component itself!

Directives, on the other hand, help us style elements that don’t have content, e.g. input . And the way they accomplish that is host binding which eliminates the necessity to list all the class names ourselves each time we define an element.

For an input field, we would write a directive in the following way

@Directive({
selector: 'input[my-input],select[my-input]',
host: {
class: 'my-control',
}
})
export class MyControl {}

This way we would need to have the classes defined globally, however, we remove the need to remember complicated class names and their combinations. Additionally, the directive would allow us to build extensible form controls in a similar way to Angular Material library ones.

Announcement. In the next article, I will explain an approach I found to be of great help while developing an application where one form would start with just 20 fields and then could be extended in live to any size a user would need.

Summarizing

  1. Introduce a UI documentation tool like Storybook
  2. Gather all the reusable UI elements in there
  3. Build many small components
  4. Use attribute selectors to have direct access to a native HTML element
  5. Acquaintance the team with the documentation tool
  6. Keep the documentation up to date

Afterword

While building components for the design system don’t forget to add them to the storybook and sync with your team. However great a library you might build it would become pointless if it’s undocumented or the team doesn’t know about its existence.

And don’t forget to set the OnPush strategy to the primitives to improve the overall performance.

--

--

Sergey Gultyayev

A front-end developer who uses Angular as a main framework and loves it