Using Tailwind to Theme Your Angular App

Written by Preston Lamb

Posted on Feb 15, 2021

angulartailwind

Getting Started with Deno

Learn how to start using Deno with my brand new course!

Buy the Course

tldr;

One of Tailwind's best features is the ability to extend its color palette and other utility classes for your application. This functionality can be used to be able to dynamically change the theme of your Angular application. In this article, you'll look at how to create an Angular service that loads a theme as your app bootstraps.

Before You Start

Before you jump in to this article, it will be helpful for you to take a look at this article which covers how to use Tailwind in your Angular application. This article will not cover how to do that. If you already know how to set up Tailwind in your project, you should be good to go.

Also, I want to thank Chau Tran for his gist, part of which I used in my service and utility functions.

Extending Tailwind's Color Palette

The key to being able to theme your application will be to extend Tailwind's color palette. This is one of the coolest features of Tailwind, and you can read about it in their docs. To do so, you need to alter the tailwind.config.js file. The file can include a theme attribute, inside of which you can add an extend attribute. The items you add to extend will be added as utility classes that you can use in your HTML and SCSS files. Here's an example config file where the colors attribute is extended.

// tailwind.config.js

module.exports = {
  theme: {
    extend: {
      colors: {
        // Here is where you will add our new colors
      }
    }
  }
}

Now that you know how to extend the colors, you'll need to decide how many colors can be changed in each of your themes. For this article, there are three colors: primary, secondary, and accent. When the application loads, three color values will need to be provided. The extended color palette will be built with those colors. We'll look at that shortly.

Now, There's another cool part of extending Tailwind's utility classes, and that is that the value for an extended class can be a CSS custom property, otherwise known as CSS variables. That's how you will be able to change the theme of the app, is by setting the color value of the classes to a CSS custom property. Then we'll use JavaScript to update those custom properties. Here's the full tailwind.config.js file, with all the classes for each color set up and ready to use.

// tailwind.config.js

module.exports = {
  theme: {
    extend: {
      colors: {
        'primary-color': {
          DEFAULT: 'var(--primary-color-500'),
          50: 'var(--primary-color-50'),
          100: 'var(--primary-color-100'),
          200: 'var(--primary-color-200'),
          300: 'var(--primary-color-300'),
          400: 'var(--primary-color-400'),
          500: 'var(--primary-color-500'),
          600: 'var(--primary-color-600'),
          700: 'var(--primary-color-700'),
          800: 'var(--primary-color-800'),
          900: 'var(--primary-color-900'),
        },
        'secondary-color': {
          DEFAULT: 'var(--secondary-color-500'),
          50: 'var(--secondary-color-50'),
          100: 'var(--secondary-color-100'),
          200: 'var(--secondary-color-200'),
          300: 'var(--secondary-color-300'),
          400: 'var(--secondary-color-400'),
          500: 'var(--secondary-color-500'),
          600: 'var(--secondary-color-600'),
          700: 'var(--secondary-color-700'),
          800: 'var(--secondary-color-800'),
          900: 'var(--secondary-color-900'),
        },
        'accent-color': {
          DEFAULT: 'var(--accent-color-500'),
          50: 'var(--accent-color-50'),
          100: 'var(--accent-color-100'),
          200: 'var(--accent-color-200'),
          300: 'var(--accent-color-300'),
          400: 'var(--accent-color-400'),
          500: 'var(--accent-color-500'),
          600: 'var(--accent-color-600'),
          700: 'var(--accent-color-700'),
          800: 'var(--accent-color-800'),
          900: 'var(--accent-color-900'),
        }
      }
    }
  }
}

For each of the three colors of the theme — primary, secondary, and accent — there are 11 values. This is what Tailwind provides on their palette, thus you should do the same. Normally, DEFAULT is the middle color, 500, but technically you can set it to what you want. What you will have now in your templates is the ability to turn a button's background to the primary color (any of the primary color values) with the class .bg-primary-color-500. You could also change the text color with .text-primary-color-400. This will be natural to you, as if you were using any of the provided colors directly from Tailwind. The values of each of these colors come from CSS custom properties. We'll set those properties momentarily.

Loading the Theme When the App Loads

There are many ways to load the theme when the app bootstraps. In this example, the theme will be loaded using the APP_INITIALIZER token when the application bootstraps. This will allow you to change the theme whenever you want, depending on where you store the theme, because the configuration is runtime and not build time. You can read more about the distinction between these types of configuration in this article. Loading the configuration when the app bootstraps takes several parts working in tandem.

The first step, after creating a TailwindThemeModule, is to create a class that will be passed in to the module that will load the theme when it's imported. Here's the class:

// tailwind-theme-config.class.ts
export class TailwindThemeConfig {
  configUrl: string;

  constructor(obj: any = {}) {
    this.configUrl = obj.configUrl || './assets/tailwind-theme.config.json';
  }
}

We'll come back to this class momentarily. Next up is the service that will load the configuration file on bootstrap and initialize the theme:

// tailwind-theme.service.ts
import { Inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { DOCUMENT } from '@angular/common';
import { TailwindTheme, updateThemeVariables } from '../tailwind-util';
import { TailwindThemeConfig } from '../tailwind-theme-config.class';
import { switchMap } from 'rxjs/operators';

@Injectable()
export class TailwindThemeService {
  constructor(
    private _http: HttpClient,
    @Inject(DOCUMENT) private readonly document: Document,
    private config: TailwindThemeConfig,
  ) {}

  loadConfig(): Promise<any> {
    const configUrl = this.config.configUrl || './assets/tailwind-theme.config.js';
    return this._http
      .get(`${configUrl}`)
      .pipe(
        switchMap((configObject: { themeUrl: string }) => {
          return this._http.get(configObject.themeUrl);
        }),
      )
      .toPromise()
      .then((themeData: TailwindTheme) => {
        updateThemeVariables(themeData, this.document);
      })
      .catch((err: any) => {
        console.error('There was an error while loading the Tailwind Theme.');
      });
  }
}

You will use this loadConfig method in a minute, and the tailwind-util files will be provided below as well. The class that we created before is injected into this service through the constructor. The loadConfig method uses that config object to know how to get the Tailwind theme configuration object. This should not be confused with the tailwind.config.js file that Tailwind proper uses; this is a custom configuration object that has information needed to load the theme. The first http.get gets that config object from a file. That file contains a themeUrl attribute that points to either another file or an API endpoint. The file can be local to the app or remote. The second http.get loads the theme information. This is where you'll get the primary, secondary, and accent colors for the theme. The return value needs to be converted to a Promise, and that's due to requirements of the APP_INITIALIZER token. The .then block gets the theme data, represented by the TailwindTheme interface and is passed to the updateThemeVariables function. Finally, errors are caught in the .catch block.

here's the TailwindTheme interface, and a Color interface as well:

// tailwind-theme.interface.ts
export interface Color {
  name: string;
  hex: string;
  isDarkContrast: boolean;
}

export interface TailwindTheme {
  'primary-color': string;
  'secondary-color': string;
  'accent-color': string;
}

If your theme uses more or less colors than primary, secondary, and accent, you'll want to add them to this interface. The next piece you need is the utility file that houses a couple useful functions for creating the color palette:

// tailwind-util.ts
import * as tinycolor from 'tinycolor2';
import { Color, TailwindTheme } from './tailwind-theme.interface';

export function computeColorPalette(hex: string): Color[] {
  return [
    getColorObject(tinycolor(hex).lighten(45), '50'),
    getColorObject(tinycolor(hex).lighten(40), '100'),
    getColorObject(tinycolor(hex).lighten(30), '200'),
    getColorObject(tinycolor(hex).lighten(20), '300'),
    getColorObject(tinycolor(hex).lighten(10), '400'),
    getColorObject(tinycolor(hex), '500'),
    getColorObject(tinycolor(hex).darken(10), '600'),
    getColorObject(tinycolor(hex).darken(20), '700'),
    getColorObject(tinycolor(hex).darken(30), '800'),
    getColorObject(tinycolor(hex).darken(40), '900'),
  ];
}

export function getColorObject(value: tinycolor.Instance, name: string): Color {
  const c = tinycolor(value);
  return {
    name,
    hex: c.toHexString(),
    isDarkContrast: c.isLight(),
  };
}

export function updateThemeVariables(theme: TailwindTheme, document: Document) {
  for (const [name, color] of Object.entries(theme)) {
    const palette = computeColorPalette(color);
    for (const variant of palette) {
      document.documentElement.style.setProperty(`--${name}-${variant.name}`, variant.hex);
    }
  }
}

The updateThemeVariables function takes in your theme interface object and loops over the keys in the object (primary-color, secondary-color, and accent-color) and creates a palette for each of them using the computeColorPalette function. Each of those color variants are looped over, and a CSS custom property is either created or updated for each value. Remember, your tailwind.config.js file uses those CSS custom property values for the extended color classes.

The last step in the TailwindThemeModule is the actual module file.

// tailwind-theme.module.ts
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TailwindThemeService } from './tailwind-theme/tailwind-theme.service';
import { TailwindThemeConfig } from './tailwind-theme-config.class';

export function initTailwindThemeConfig(tailwindThemeSvc: TailwindThemeService) {
  return () => tailwindThemeSvc.loadConfig();
}

@NgModule({
  imports: [CommonModule],
  providers: [
    TailwindThemeService,
    {
      provide: APP_INITIALIZER,
      useFactory: initTailwindThemeConfig,
      deps: [TailwindThemeService],
      multi: true,
    },
  ],
})
export class TailwindThemeModule {
  static forRoot(config: TailwindThemeConfig): ModuleWithProviders<TailwindThemeModule> {
    return {
      ngModule: TailwindThemeModule,
      providers: [
        {
          provide: TailwindThemeConfig,
          useValue: config,
        },
        TailwindThemeService,
      ],
    };
  }
}

This module file registers the TailwindThemeService and uses the APP_INITIALIZER token to call the loadConfig method. This function will be called when the app is imported and when the application is being initialized. In addition, the module allows for passing the TailwindThemeConfig object in to the module when importing it. That is what the static forRoot method is for.

This is all you need to create your Tailwind theme service. You can find all these files in full at this gist.

Importing the TailwindThemeModule

After you create your TailwindThemeModule, it needs to be imported into the main AppModule to be used. Here's one way to do this:

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { TailwindThemeModule } from './tailwind-theme';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    TailwindThemeModule.forRoot({ configUrl: './assets/config/tailwind-theme.config.json' }),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

The module is imported, and an object provided in the forRoot method. That object has the configUrl that is required on the TailwindThemeConfig class. The value can be a string literal like this, but then that config (which, again, provides the location of the Tailwind theme colors) needs to be located in the same place in every environment. You could also use the environment file and change the location based on environment.

Running Your Application

Now that the TailwindThemeModule is imported, you can start your application. Serve it locally, and then open the app.component.html file. Add a few paragraphs of text to the component, each with a different Tailwind class. You could do something like this:

<!-- app.component.html -->
<p class="text-primary-color-500">This will be the primary color.</p>
<p class="text-secondary-color-500">This will be the secondary color.</p>
<p class="text-accent-color-500">This will be the accent color.</p>

In addition to the 500 color value, you should have the 50 and 100-900 values.

Conclusion

At this point, you now have the ability to theme your application with three different colors: primary, secondary, and accent. As long as the colors in the design can be used based off these three colors, you should be good to go. If not, you can add more accent colors in the same manner as you did above. Also, this article just shows you how to change the theme when the application bootstraps, but using the updateThemeVariables function would allow you to change the theme at any time. Again, this is because the extended colors values are based on CSS custom properties.

Getting Started with Deno

Learn how to start using Deno with my brand new course!

Buy the Course