Reusable Buttons with Angular, Tailwind & Storybook
Tags: angular, tailwindcss, tutorial, storybook
Published: 2022-11-28 06:30:14


## What will we build?


We will build a custom Angular directive that applies our style systems button design to the element it is applied to.

![Storybook view of button directive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z9lqpcevu7f9dveiz64b.png)

### Why a directive?

- You get all the benefits of tailwind and its powerful styling techniques without losing the accessibility coming from the default html elements.
- With a directive your styles are not tied to the button element. Other elements, like links/anchors, can also look like a button. This flexibility makes it easier for you to write great looking and accessible applications & websites.

### Why Tailwind?

- Its set of utility classes have good design principles built in.
- Building your reusable UI components with tailwind gives you consistency and flexibility out of the box
- You can use Angular’s directives and components to hide the often complained about ugly markup.

## The code.

This tutorial needs an Angular project that is configured to run with Tailwind (and Storybook if you want to get the interactive demo from above). 

### The setup.

The easiest way to follow along is by forking my project on [Github](https://github.com/goetzrobin/nx-angular-tailwind) and checking out the `0-base-storybook-setup` branch

You will find an NX project with an `example` app and a `libs` with a `ui` subdirectory, which contains the `atoms` directory and the `storybook` library. 

The `storybook` library allows you to run the `npx nx run ui-storybook:storybook` (or `nx run ui-storybook:storybook` if you have nx installed globally) command. This generates all stories in the `ui` directory. It is also configured to correctly render all Tailwind classes.

The `atoms` directory (based on Brad Frost’s [atomic design principles](https://atomicdesign.bradfrost.com/chapter-2/) finally contains our `button` library, the entry point of today's tutorial.

Let’s see if we wired up our storybook and tailwind config correctly.

### Let's get started!

We create a simple attribute directive that adds a blue background color to our button.

Create a directory named `button` in the src directory.

Add the `button.directive.ts` file in this newly created folder.

Add the following contents to the file:

```ts
import {Directive} from "@angular/core";

@Directive({
  selector: '[natButton]',
  standalone: true,
})
export class ButtonDirective {
}

Note: As you see we are using Angular’s new standalone config. If you are using Modules make sure to add your directive to the declarations and exports arrays
```
Next, we will use Angular’s HostBinding decorator & a private `twClasses` variable to bind the tailwind utility class `bg-sky-500` to our host elements class attribute

```ts
import {Directive, HostBinding} from "@angular/core";

@Directive({
  selector: '[natButton]',
  standalone: true,
})
export class ButtonDirective {
  @HostBinding('class')
  private twClasses = 'bg-sky-500'
}
```

So far so good! Let’s add `button.stories.ts` in the same directory and write a simple story to see if storybook renders our new button directive correctly.

```ts
import {ButtonDirective} from "./button.directive";
import {Meta, Story} from "@storybook/angular";

export default {
  title: 'Design System/Atoms/Button',
} as Meta<ButtonDirective>;

export const Default: Story<ButtonDirective> = () => ({
  moduleMetadata: {
    imports: [ButtonDirective],
  },
  template: `<button natButton>Click</button>`
});
```
Awesome! You just created your first story following the [Component Story Format.](https://storybook.js.org/docs/angular/api/csf) Let’s break down the code:
- Our default export tells storybook the metadata of our stories. Following storybooks naming convention we create our _Button_ stories in the _Atoms_ directory of our _Design System_ project.
- We then export the _Default_ story for our ButtonDirective. To configure the story we add the directive to the `moduleMetadata` array and the `button` element in our template.

Lets run the `npx nx run ui-storybook:storybook` command and see if we get our Click button with the sky blue background.

We open `localhost:4400`, navigate into the Atoms folder and select our Default Button Story.

![Storybook UI displays Click button with blue background](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9nqmnmf0zd9e1pkzj50.png)

Awesome! Let's take this button to the next level.

### Making it real.

So far our button is not very impressive. 

So let’s grab some designs from our (imaginary) designer friends and give our directive superpowers. Luckily, I prepared some good looking buttons for you [here.](https://play.tailwindcss.com/DDXuw6Y4ln)

Copy the classes of the primary button and add them to our directive.

```ts
import {Directive, HostBinding} from "@angular/core";

@Directive({
  selector: '[natButton]',
  standalone: true,
})
export class ButtonDirective {
  @HostBinding('class')
  private twClasses = `inline-flex items-center rounded-md border border-transparent bg-sky-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2`
}
```
Refresh your storybook page and see how beautiful our button looks now.


![Primary styled button](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mly5mmaukc0plopz110q.png)


### Making it smart.

While our button looks great, so far we just copied around a bunch of tailwind classes.

Time to make our directive smart and support multiple themes and sizes.

First, let’s split our long twClasses string into smaller groups based on their functionality.

```ts
import {Directive, HostBinding} from "@angular/core";

const base = 'inline-flex items-center rounded-md border font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2'
const size = 'px-4 py-2 text-sm'
const theme = 'border-transparent bg-sky-600 text-white hover:bg-sky-700 focus:ring-sky-500'

@Directive({
  selector: '[natButton]',
  standalone: true,
})
export class ButtonDirective {
  @HostBinding('class')
  private twClasses = `${base} ${size} ${theme}`
}
```

Much better! Can you guess where we are going with this?
Let’s add an input property to our directive that lets us change the theme!
```ts
import {Directive, HostBinding, Input} from "@angular/core";

const base = 'inline-flex items-center rounded-md border font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2'
const size = 'px-4 py-2 text-sm'
const theme = 'border-transparent bg-sky-600 text-white hover:bg-sky-700 focus:ring-sky-500'
const themeSecondary = 'border-gray-300 bg-white text-gray-700 hover:bg-sky-700 focus:ring-gray-50'

export type Theme = 'primary' | 'secondary';

const buildTwClasses = (currentTheme: Theme): string =>
  `${base} ${size} ${currentTheme === 'secondary' ? themeSecondary : theme}`;

@Directive({
  selector: '[natButton]',
  standalone: true,
})
export class ButtonDirective {
  @HostBinding('class')
  private twClasses = buildTwClasses('primary')

  private _theme: Theme = 'primary';
  @Input()
  set theme(value: Theme) {
    this._theme = value;
    this.twClasses = buildTwClasses(this._theme);
  }
}

```
What exactly is happening in this code? 
- We added the `theme` input to our directive which takes in a parameter of type `Theme`, which is our currently supported themes, `primary` and `secondary`.
- There is a `buildTwClasses` helper function that builds the tailwind classes for our buttons based on the `currentTheme` we pass in.
- Our `set` function rebuilds the `twClasses` every time the theme input changes and assigns it to our `twClasses` property, which is bound to the host's class property.

Let’s manually change our story template to use the secondary theme and see if our code works.

```ts
import {ButtonDirective} from "./button.directive";
import {Meta, Story} from "@storybook/angular";

export default {
  title: 'Design System/Atoms/Button',
} as Meta<ButtonDirective>;

export const Default: Story<ButtonDirective> = () => ({
  moduleMetadata: {
    imports: [ButtonDirective],
  },
  template: `<button natButton theme="secondary">Click</button>`
});
```
Refresh the page to see if the changes are picked up.

![Storybook UI showing the secondary button style](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dczko9vtv3lbw4vebxng.png)

They are!! If you got this far you are now equipped with the fundamental knowledge to build beautiful buttons or links that look like buttons or anything you want that looks like buttons. 

However, there are a lot of improvements and enhancements we can add. So if I sparked your interest keep on reading!

### Making it super smart

So far we have a decent button directive. However, as our design system grows and designers are adding large buttons used in CTA’s of the new version of our app. They also pointed out that there is no visual hint to when a button is disabled. Let’s update our `button.directive.ts` and make sure we add some styles for that to our button base.

```ts
const base = `inline-flex items-center rounded-md border font-medium shadow-sm
  focus:outline-none focus:ring-2 focus:ring-offset-2
  disabled:opacity-50 disabled:focus:ring-0 disabled:active:ring-0`;
```

Next, we add a new input property called size to our directive.
It accepts inputs of type `Size`, which is our supported sizes, `base` and `l`.
While we are here let’s refactor our Tailwind configurations. 

Instead of toggling between string constants, let's create objects that hold the class strings organized by the `Theme` and `Size` inputs respectively. 

```ts
export const themes = ['primary', 'secondary'] as const;
export type Theme = typeof themes[number];
const themeClasses: { [key in Theme]: string } = {
  primary: 'border-transparent bg-sky-600 text-white hover:bg-sky-700 disabled:bg-sky-600 focus:ring-sky-500',
  secondary: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:bg-white focus:ring-sky-500'
}

export const sizes = ['base', 'l'] as const;
export type Size = typeof sizes[number];
const sizeClasses: { [key in Size]: string } = {
  base: 'px-4 py-2 text-sm',
  l: 'px-5 py-3 text-base'
};
```

Did you notice that we create those types based on constant string arrays of our supported themes & sizes. As we add support for sizes and themes our types will magically adjust! Super smart!

In our `buildTwClasses` function we can now use our inputs as keys to look up the respective classes. Using this approach we can easily add new themes or sizes as our design grows.

```ts
const buildTwClasses = (currentTheme: Theme, size: Size): string =>
  `${base} ${sizeClasses[size]} ${themeClasses[currentTheme]}`;
```

Our directive with all its inputs on configuration now looks like this:

```ts
import {Directive, HostBinding, Input} from "@angular/core";

const base = `inline-flex items-center rounded-md border font-medium shadow-sm
  focus:outline-none focus:ring-2 focus:ring-offset-2
  disabled:opacity-50 disabled:focus:ring-0 disabled:active:ring-0`;

export const themes = ['primary', 'secondary'] as const;
export type Theme = typeof themes[number];
const themeClasses: { [key in Theme]: string } = {
  primary: 'border-transparent bg-sky-600 text-white hover:bg-sky-700 disabled:bg-sky-600 focus:ring-sky-500',
  secondary: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 disabled:bg-white focus:ring-sky-500'
}

export const sizes = ['base', 'l'] as const;
export type Size = typeof sizes[number];
const sizeClasses: { [key in Size]: string } = {
  base: 'px-4 py-2 text-sm',
  l: 'px-5 py-3 text-base'
};

const buildTwClasses = (currentTheme: Theme, size: Size): string =>
  `${base} ${sizeClasses[size]} ${themeClasses[currentTheme]}`;

@Directive({
  selector: '[natButton]',
  standalone: true,
})
export class ButtonDirective {
  @HostBinding('class')
  private twClasses = buildTwClasses('primary', 'base');

  private _theme: Theme = 'primary';
  @Input()
  set theme(value: Theme) {
    this._theme = value;
    this.twClasses = buildTwClasses(this._theme, this._size);
  }

  private _size: Size = 'base';
  @Input()
  set size(value: Size) {
    this._size = value;
    this.twClasses = buildTwClasses(this._theme, this._size);
  }
}
```
Again, we can manually update our story, checking if our new size input is picked up correctly.
```ts
import {ButtonDirective} from "./button.directive";
import {Meta, Story} from "@storybook/angular";

export default {
  title: 'Design System/Atoms/Button',
} as Meta<ButtonDirective>;

export const Default: Story<ButtonDirective> = () => ({
  moduleMetadata: {
    imports: [ButtonDirective],
  },
  template: `<button natButton theme="secondary" size="l">Click</button>`
});
```
Perfect! A large button with our secondary theme is displayed!


![A large button with our secondary theme is displayed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4yg68ojld4nhoxaowkvy.png)


### Leveling up our storytelling.

Right now we are still updating our stories file a lot to preview our changes. Wouldn’t it be great to be able to see all the different button styles update dynamically in our storybook? Let’s make it happen.

In our `button.stories.ts` file, we create a `ButtonDirectiveProps` type that will drive our story controls. It extends the `ButtonDirective` with the `content` property.

```ts
type ButtonDirectiveProps = ButtonDirective & {
  content: string;
};
```

We then specify that we want a `select` control for our size & theme inputs and tell storybook to populate their options with the supported sizes & themes respectively.

```ts
export default {
  title: 'Design System/Atoms/Button',
  argTypes: {
    size: {
      control: { type: 'select' },
      options: sizes,
    },
    theme: {
      control: { type: 'select' },
      options: themes,
    },
  },
} as Meta<ButtonDirectiveProps>;
```

Next, we add some default args for our stories. Let’s take the first of each input options and set our content to _Angular & Tailwind rock_.

```ts
const DefaultArgs = {size: sizes[0], theme: themes[0], content: 'Angular & Tailwind rock'};
```

We then rename our `Default` story to `Template` and add the args parameter to the function. Our args will be of type `ButtonDirectiveProps`, which we extend with our button specific `disabled` property and then bind to the stories props. We update our template to bind our angular inputs to the new props we provide. 
Lastly, we copy the `Template` as described in the [Storybook introduction](https://storybook.js.org/docs/angular/writing-stories/introduction) and set the stories arguments to our defaults, finally setting the `disabled` property to `false`.

```ts
const Template: Story<ButtonDirectiveProps & {  disabled: boolean; }> = (args) => ({
  props: args,
  moduleMetadata: {
    imports: [ButtonDirective],
  },
  template: `<button natButton [disabled]="disabled" [size]="size" [theme]="theme">{{content}}</button>`
});

export const Default = Template.bind({})
Default.args = {...DefaultArgs, disabled: false};
```

Let’s reload our stories at localhost:4400.

![Storybook view of button directive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z9lqpcevu7f9dveiz64b.png)

Did you see the controls appear and our button updating when we change any of them!

I mentioned that the great thing about directives is that we can apply them to a variety of tags. Let’s see what happens if we add our directive to an anchor tag. 

We copy our Template and Default export. 

Change the disabled property to one called link and rename our Template to Anchor template and our export to Anchor. Lastly, we wire up our link property with the anchor tags href property and add a link to the stories args.

```ts
const AnchorTemplate: Story<ButtonDirectiveProps & {link: string}> = (args) => ({
  props: args,
  moduleMetadata: {
    imports: [ButtonDirective],
  },
  template: `<a target="_blank" [href]="link" natButton [size]="size" [theme]="theme">{{content}}</a>`
});

export const Anchor = AnchorTemplate.bind({})
Anchor.args = {...DefaultArgs, link: 'https://media.giphy.com/media/jJQC2puVZpTMO4vUs0/giphy.gif'};
```

Reload your storybook one more time and check out the newly added Anchor story. Click on the anchor disguised as our button and you’ll see that it works as expected.

![Congratulations! You made it.](https://media.giphy.com/media/jJQC2puVZpTMO4vUs0/giphy.gif)

You made it to the end of this tutorial and built an awesome button directive. And did notice how clean our stories markup was? No Tailwind classes in the markup.

## What’s next?

This is my first time writing an article like this. So first of all, thanks for reading! I hope you learned something new! What’s next? You decide. Did you enjoy this article? Are you interested in learning about the Storybook setup I use for this project? What UI element should we create next? Would you rather have a video tutorial you can follow along with? Let me know in the comments.