How to integrate Tailwind and Angular Material Themes
As an Angular developer, I find myself in a committed relationship with Angular Material. At the same time, I try to maximize my use of Tailwind CSS. Here are my guidelines for maintaining single-source-of-truth and consistent theming between the two frameworks while providing the extensibility to define completely arbitrary themes as needed.
Step #0: Include Angular Material and Tailwind in an Angular project or NX monorepo containing an Angular project
An Angular project with Tailwind configured is a prerequisite for this tutorial.
Step #1: Define SASS/SCSS variables corresponding to the relevant parameters exposed by Material
The purpose of creating these SASS/SCSS variables is primarily for use in component style definitions (think styles/styleUrls in an @Component invocation) so that custom components can reference the same colors Material is using without conventionally extracting the variables from the Material configuration via SASS/SCSS functions. Secondly, these variables provide a concrete compilation target (ie I get an error if I make a mistake).
The variable declaration should be something like this:
// name this file _colors.scss or _variables.scss or _whatever-you-want.scss
// this is my base text color and a subtle-text counterpart
// i'm not passing this into material... but could if desired
$base: var(--base);
$base-muted: var(--base-muted);
// this is my contrast text color and a subtle-text counterpart
// i'm not passing this into material... but could if desired
$contrast: var(--contrast);
$contrast-muted: var(--contrast-muted);
// these 3 pairs of color variables are a minimalistic representation of conventional material color variables
$accent: var(--accent);
$accent-contrast: var(--accent-contrast);
$primary: var(--primary);
$primary-contrast: var(--primary-contrast);
$warn: var(--warn);
$warn-contrast: var(--warn-contrast);
Step #2: Define a custom Material theme comprised strictly of the SASS/SCSS variables created in the previous step
The Material theme should look like this:
// name this file _themes.scss or something like that
@use 'sass:map';
@use '@angular/material' as mat;
@import '../common/_colors';
$primary-palette: (
mat.define-palette(
(
100: $primary,
500: $primary,
700: $primary,
contrast: (
100: $primary-contrast,
500: $primary-contrast,
700: $primary-contrast,
),
)
)
);
$accent-palette: (
mat.define-palette(
(
100: $accent,
500: $accent,
700: $accent,
contrast: (
100: $accent-contrast,
500: $accent-contrast,
700: $accent-contrast,
),
)
)
);
$warn-palette: (
mat.define-palette(
(
100: $warn,
500: $warn,
700: $warn,
contrast: (
100: $warn-contrast,
500: $warn-contrast,
700: $warn-contrast,
),
)
)
);
$theme: (
color: (
primary: $primary-palette,
accent: $accent-palette,
warn: $warn-palette,
foreground: mat.$light-theme-foreground-palette,
background: mat.$light-theme-background-palette
),
);
$dark-theme: (
color: (
primary: $primary-palette,
accent: $accent-palette,
warn: $warn-palette,
foreground: mat.$dark-theme-foreground-palette,
background: mat.$dark-theme-background-palette
),
);
Step #3: Instantiate the Material theme(s) using the Material SCSS/SASS mixins
// name this file something like _material.scss
@use 'sass:map';
@use '@angular/material' as mat;
@include mat.core();
@import '../common/_colors'; // or whatever you named this
@import 'themes'; // or whatever you named this
// force material to use theme colors for raised/flat button text color
a,
button {
&[mat-raised-button], &[mat-flat-button] {
&[color=primary] {
color: $primary-contrast !important;
}
&[color=accent] {
color: $accent-contrast !important;
}
&[color=warn] {
color: $warn-contrast !important;
}
}
}
// now we add the two themes we created
// default theme
@include mat.all-component-themes($theme);
// conditionally apply theme via a CSS class
.dark-theme {
@include mat.all-component-themes($dark-theme);
}
Step #4: Define corresponding Tailwind variables in tailwind.config.js
The tailwind.config.js file should contain similarly named variables that pull from the same CSS variable as the SCSS equivalent defined in Step #1. This allows us to use the Tailwind utility classes for color and background color. For the example I’m providing, the config should look like this:
const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
const { join } = require('path');
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
...createGlobPatternsForDependencies(__dirname),
],
theme: {
extend: {
colors: {
'base': 'var(--base)',
'base-muted': 'var(--base-muted)',
'contrast': 'var(--contrast)',
'contrast-muted': 'var(--contrast-muted)',
'accent': 'var(--accent)',
'accent-contrast': 'var(--accent-contrast)',
'primary': 'var(--primary)',
'primary-contrast': 'var(--primary-contrast)',
'warn': 'var(--warn)',
'warn-contrast': 'var(--warn-contrast)'
}
},
},
plugins: [],
};
Step #5: Import the Material theme and define CSS variables in the root SASS/SCSS file of your Angular project (usually styles.scss)
This is where you use everything defined in the previous steps. This works for a single Angular app or multiple Angular apps in a given project. Note that the configurations from previous steps are only defined once and re-used whether consumed in one app or multiple apps. However, this step will need to be completed for each app (unless you want them all to share the same themes in which case you would only define this file once in a common directory and re-import the file into each app’s styles.scss file). The styles.scss should contain the following:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--base: rgba(0, 0, 0, 0.8);
--base-muted: rgba(0, 0, 0, 0.54);
--contrast: rgba(255, 255, 255, 0.8);
--contrast-muted: rgba(255, 255, 255, 0.54);
--accent: rgb(250, 160, 64);
--accent-contrast: #ffffff;
--primary: rgb(7, 108, 145);
--primary-contrast: #ffffff;
--warn: rgb(205, 62, 71);
--warn-contrast: #ffffff;
color: var(--base);
.dark-theme {
--contrast: rgba(0, 0, 0, 0.8);
--contrast-muted: rgba(0, 0, 0, 0.54);
--base: rgba(255, 255, 255, 0.8);
--base-muted: rgba(255, 255, 255, 0.54);
--primary: hotpink;
--accent: limegreen;
color: var(--base);
}
.usa-theme {
--base: #002868;
--base-muted: #0028689c;
--contrast: rgba(255, 255, 255, 0.8);
--contrast-muted: rgba(255, 255, 255, 0.54);
--accent: #BF0A30;
--accent-contrast: #002868;
--primary: #002868;
--primary-contrast: #ffffff;
--warn: #BF0A30;
--warn-contrast: #ffffff;
color: var(--base);
}
}
}
// import the file created in step #3 (named something like _material.scss)
// it would probably look like this for a single Angular app scenario
@import 'material';
// ... or for multi-app projects using a common config...
// just use the full path from the app to the other lib with the file
// @import '../../../../libs/ui/common/src/lib/styles/_material.scss';
Step #6: Use the created themes in Angular Material or Tailwind… or BOTH!
Given the following test component template that uses both Tailwind utility color classes (bg-blah or text-blah or border-blah where “blah” is a defined color variable in tailwind.config.js):
<mat-card class="p-4 flex flex-col gap-8 mat-elevation-z8">
<div class="p-2">This is a component with some Material buttons and Tailwind text.</div>
<div class="p-2 mat-elevation-z4 rounded text-lg font-bold bg-primary-contrast text-primary border-solid border-4 border-primary">primary - this uses tailwind utility classes</div>
<div class="flex flex-row gap-2 py-4">
<button mat-button color="primary">button</button>
<button mat-raised-button color="primary">raised</button>
<button mat-flat-button color="primary">flat</button>
<button mat-stroked-button color="primary">stroked</button>
</div>
<div class="p-2 mat-elevation-z4 rounded text-lg font-bold bg-accent-contrast text-accent border-solid border-4 border-accent">accent - this uses tailwind utility classes</div>
<div class="flex flex-row gap-2 py-4">
<button mat-button color="accent">button</button>
<button mat-raised-button color="accent">raised</button>
<button mat-flat-button color="accent">flat</button>
<button mat-stroked-button color="accent">stroked</button>
</div>
<div class="p-2 mat-elevation-z4 rounded text-lg font-bold bg-warn-contrast text-warn border-solid border-4 border-warn">warn - this uses tailwind utility classes</div>
<div class="flex flex-row gap-2 py-4">
<button mat-button color="warn">button</button>
<button mat-raised-button color="warn">raised</button>
<button mat-flat-button color="warn">flat</button>
<button mat-stroked-button color="warn">stroked</button>
</div>
<mat-form-field>
<mat-label>Do you love it?</mat-label>
<input matInput type="text" />
</mat-form-field>
</mat-card>
And given the following invocations of that component with our theming classes:
<div class="flex flex-row justify-around">
<test></test>
<test class="dark-theme"></test>
<test class="usa-theme"></test>
</div>
You should see the separate themes rendered with consistent coloring between Tailwind and Material:
Step #7: Theme all the things
This tutorial is only a superficial serving-suggestion. If you’re creative, you’ve probably already realized how many different variations could be applied to this conceptual framework detailed in this article. For example, you can modify CSS variables inside of individual components or create a component that encapsulates predefined or overrideable values for the CSS variables. Another example is being able to pass-in values for these variables from consuming code if you’re writing a library or creating Angular apps for embedding into other apps/pages (which is my use-case). The list goes on and on!
Step #8: Learn more
This video is pure excellence if you’d like to learn even more advanced techiniques for theming Tailwind:
https://www.youtube.com/watch?v=MAtaT8BZEAo&t=782s
Tailwind documentation: https://tailwindcss.com/docs
Material Source Code: https://github.com/angular/components