This is something I’ve been using in projects I work on, while using Tailwind CSS.
With this approach, we define all our design tokens once as variables and map them to their underlying colors. We can use media queries (e.g. for dark mode), to vary the values of these variables.
This allows us to build dark-mode supporting sites without requiring the overhead of applying tons of dark:
styles across the whole codebase. As a side benefit, we can also make global theme updates by modifying our design tokens, not every single component that uses the colors.
Before we start
There’s three separate pieces to this approach:
- Extract the color scales from Tailwind to CSS Variables with a plugin.
- Implement design tokens as their own variables in CSS, that reference the original Tailwind colors.
- Extend Tailwind’s theme to add new “colors” for each of these design tokens which reference our variables.
Exporting Colors as Variables
First, we need to advertise the Tailwind color scales as CSS variables. This GIST by @Merott on Github contains a plugin that accomplishes this, (reproduced below), with some small modifications to keep TypeScript happy. This exposes colors to variables such as --color-blue-500
:
plugins: [
// https://gist.github.com/Merott/d2a19b32db07565e94f10d13d11a8574
// add to your tailwind CSS plugins
function({ addBase, theme }: any) {
function extractColorVars(colorObj: any, colorGroup = '') {
return Object.keys(colorObj).reduce((vars: any, colorKey: any) => {
const value = colorObj[colorKey];
const newVars : any =
typeof value === 'string'
? { [`--color${colorGroup}-${colorKey}`]: value }
: extractColorVars(value, `-${colorKey}`);
return { ...vars, ...newVars };
}, {});
}
addBase({
':root': extractColorVars(theme('colors')),
});
},
]
Creating Design Tokens
Add declarations for all your tokens as normal CSS variables in your main CSS file. (I use the file containing the @tailwind
declarations).
One could imagine other ways to use this to enable multiple themes besides dark/light using classes instead of prefers-color-scheme
.
:root {
--background: var(--color-neutral-100);
--text-primary: var(--color-neutral-950);
--primary: var(--color-blue-500);
--surface: var(--color-neutral-100);
--surface-hover: var(--color-neutral-200);
/* .. and so on */
}
/* your dark theme */
@media (prefers-color-scheme: dark) {
:root {
--background: var(--color-neutral-950);
--text-primary: var(--color-neutral-100);
--primary: var(--color-blue-500);
--surface: var(--color-neutral-900);
--surface-hover: var(--color-neutral-800);
/* .. and so on *//
}
}
Adding as Colors
Next, add these as actual colors in your Tailwind config file, within the theme colors section. This will expose the colors as named colors you can use within your app.
theme: {
extend: {
colors: {
'background': "var(--background)",
'text-primary': "var(--text-primary)",
'primary': "var(--primary)",
'surface': "var(--surface)",
'surface-hover': "var(--surface-hover)",
}
}
}
Using in your components
Now, instead of writing something like: bg-neutral-100 dark:bg-neutral-950
, you can simply write something like: bg-background
, which will work for both light/dark mode. Additionally, if you decide to change it, you can easily do so.
And - if you decide to change your color scheme down the line, all that’s needed is to update the definition of the token in your main CSS file to its new value.
<!-- this -->
<div class="bg-neutral-100 dark:bg-neutral-950"><!--... ---></div>
<!-- becomes -->
<div class="bg-background"><!--... ---></div>
Why Tailwind?
I used to be against things like Tailwind, preferring manually crafted CSS. However, I’ve found it to be very useful especially when dealing with components, and it makes it very easy to reason about how a component is styled.
When opening a component’s markup in something like React to make changes, all the information you need to understand it is contained within. There are no separate styles applied globally that might conflict with things and changes to this component don’t also unintentionally impact other areas.