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.
Top comments (0)