Theming with Tailwind
Most modern applications allow users to switch between multiple themes. Tailwind provides a dark: modifier to handle light and dark modes, however this approach quickly becomes limiting when you want more than one theme. For example, apps like Trello or Linear let users choose from several themes, rather than just light and dark.
Relying solely on dark: also doesn't abstract your styles into design tokens, which can make scaling and maintaining your UI difficult. There may also be cases where a component should always render in a particular theme regardless of the user's system preference.
The tw-colors plugin allows you to generate Tailwind classes scoped to a parent theme. You add it to your tailwind.config file like this:
import type { Config } from "tailwindcss";
import { createThemes } from "tw-colors";
import { light } from "./src/themes/light.theme";
import { dark } from "./src/themes/dark.theme";
const config: Config = {
content: ["./src/**/*.{js,ts,jsx,tsx,mdx}"],
plugins: [
createThemes({
light: {
"background-primary": "white",
"text-primary": "black",
"text-secondary": "gray",
},
dark: {
"background-primary": "black",
"text-primary": "white",
"text-secondary": "gray",
},
}),
],
};
export default config;
This will generate the following CSS:
.light,
[data-theme="light"] {
--background-primary: white;
--text-primary: black;
--text-secondary: gray;
}
.dark,
[data-theme="dark"] {
--background-primary: black;
--text-primary: white;
--text-secondary: gray;
}
.bg-background-primary {
background-color: var(--background-primary);
}
.text-text-primary {
color: var(--text-primary);
}
.text-text-secondary {
color: var(--text-secondary);
}
Which can be used like so:
const Panel = () => {
return (
<div className="bg-background-primary rounded p-4">
<h2 className="text-text-primary text-xl">Panel Heading</h2>
<p className="text-text-secondary">
This is some secondary text inside the panel.
</p>
</div>
);
};
What's nice about this approach is that you can define your tokens in a type-safe way, ensuring that all tokens are properly specified:
export enum Themes {
light = "light",
dark = "dark",
}
export type Theme = {
"background-primary": string;
"background-secondary": string;
"border-primary": string;
"border-secondary": string;
"button-primary-background": string;
"button-primary-border": string;
"button-primary-text": string;
"text-primary": string;
"text-secondary": string;
"codeblock-background": string;
"selection-background": string;
"selection-text": string;
};
We can then use our newly created theme type to create themes. If you plan on using Tailwind's default palettes, you can reference them in the following way:
import { Theme } from "@/themes";
import { black, neutral, white } from "tailwindcss/colors";
export const dark: Theme = {
"background-primary": neutral[800],
"background-secondary": neutral[700],
"border-primary": neutral[700],
"button-primary-background": white,
"button-primary-border": black,
"button-primary-text": black,
"text-primary": white,
"text-secondary": neutral[400],
};
As an extra precaution, you can use eslint-plugin-tailwindcss to configure ESLint to throw errors whenever an invalid or non-existent Tailwind class is used.