How to make a button in Angular

Sergey Gultyayev
4 min readJan 22, 2022

Seems like a simple question. Just write a few global classes and use them. Then you find that the design requires a button to be outlined as well, also it could be displayed as a link, also it could be bigger and smaller.

Eventually, you end up with a class name like btn btn-sm btn-secondary and maybe some more. It’s not only cumbersome to use, but also requires you to remember all the classes it has. New developers may end up creating the same button styles with a different name.

Transforming button classes to a component

How can we solve it? Use the approach that UI libraries/frameworks enable us to use — component-based approach. For this, we need to write an Angular component. How would we do that? We could write the following code

@Component({
selector: 'my-button',
template: `
<button
[ngClass]="{
'btn-secondary': isSecondary,
'btn-outline': isOutline,
'btn-sm': isSmall
}"><ng-content></ng-content></button>
`,
styles: ['...move global styles to the button component']
})
export class MyButtonComponent {
@Input() isSecondary = false;
@Input() isOutline = false;
@Input() isSmall = false;
}

This way we would reduce the perceptional complexity and have an autocompletion for components inputs.

But at the same time, we added a new problem. How would we add an id attribute to the button? We would need to add an input, which also is named differently than id so we don’t end up with the same id used twice in the DOM. What if we also need to add some ARIA attribute(s)? We would need to add even more inputs. See the problem? Any time we need to add something new to the underlying button we have to change the component which breaks the Open-Closed principle.

Refactoring the component

Can we improve that? Indeed. For this, we will need to transform the component a bit.

@Component({
selector: 'button[my-button],a[my-button]',
template: `<ng-content></ng-content>`,
styles: ['...']
})
export class MyButtonComponent {
@Input()
@HostBinding('class.btn-secondary')
isSecondary = false;

@Input()
@HostBinding('class.btn-outline')
isOutline = false;

@Input()
@HostBinding('class.btn-sm')
isSmall = false;
}

Now the usage will look like <button my-button>Submit</button> . This way we always have the native button at hand. We can have as many attributes as we want on it without modifying our component.

Notice that we also specified a in the component selector. This is done intentionally because we always should follow the semantics. Even if a button looks like a link, but it uses a click handler instead of href it’s a button. The same goes for a link — it can look like a button, but it should remain a link. This is essential for accessibility and for the UX and doesn’t cost us much effort to implement it the right way.

Preparing button for icons

We can improve the button even more — use grids. Sounds insane, huh? Not quite. In CSS we don’t have any semantics and thus we are not limited to using block, display-block, inline for buttons. We should use what makes our life easier. Why grids? It enables us to prepare the button for use cases when it has to contain icons in an easy way. What do we need to do? Modify host styles

:host {
display: inline-grid;
grid-auto-flow: column;
gap: 10px;
align-items: center;
justify-content: center;
}

Let’s review what we have written here:

  • display: inline-grid we preserve the default button behaviour by making it inline but also enabling grid capabilities
  • grid-auto-flow: column this makes all new items in the grid container be added as a new column instead of a row (they will go in one line)
  • gap: 10px we add 10px of space between items in the button
  • align-items: center center content vertically
  • justify-content: center center content horizontally

After that, we can use icons in our buttons and not use any additional classes for the correct alignment.

Make button styles override easy

We can go even further and add CSS custom properties. They will allow us to override styles without playing with selector specificity or !important .

:host {
background-color: var(--btn-theme-bg-color, rgb(20, 158, 2));
color: var(--btn-theme-color, white);
gap: var(--btn-gap, 10px);
}

Thanks to this approach we define the default appearance of the button and at the same time, we allow to override styles from the parent component just by providing CSS custom properties. For example

<div style="--btn-theme-bg-color: orange; --btn-gap: 5px">
<button my-button>
<img src="..."> Get info
</button>
<button my-button>
<img src="..."> Get info
</button>
</div>

By providing two properties on the parent component we overrode the looks of all children buttons. Cool right?

You can explore the resulting code here

--

--

Sergey Gultyayev

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