Setting Up Themes with Tailwind CSS and React
- Author
- Sean Keever
- Last modified
- Sep 19, 2020
- Time to read
- 2 min read
Photo by Robert Keane
Table of Contents
Context
In this blog post, we will look at how we are able to build a theme switcher like the following.
In particular, we'll look at:
- How to define semantic colors with Tailwind.
- How to create different themes for your site.
Note
This article assumes some familiarity with Tailwind CSS and React, but you should be able to apply the patterns to any flavor of CSS and JavaScript.
By default, Tailwind gives us color classes using the name and shade of color.
<button class="bg-blue-500 text-gray-100">Button</button>
This works fine for many projects, but if we want our project to support multiple themes, it can be helpful to use semantic color names.
<button class="bg-primaryBg text-onPrimaryBg">Button</button>
Instead of hard-coding the name of a color, we create an abstraction to give us the flexibility to easily change the theme of the site.
We define 2 semantic types:
- Primary—this is our brand's colors. For example, Twitter's primary color is blue.
- Neutral—this is our neutral color—gray in most cases/
For each semantic type, we define variants to handle different shades of color.
Pick variants that make sense for you and your project.
Below are the variants that I've defined in action. Try switching the theme to see how it changes.
- bg-primaryBgtext-onPrimaryBgSofter
- bg-primaryBgSofttext-onPrimaryBgSoft
- bg-primaryBgSoftertext-onPrimaryBg
- bg-primarySofttext-white
- bg-primarytext-white
- bg-primaryBoldtext-white
- bg-neutralBgtext-onNeutralBgSofter
- bg-neutralBgSofttext-onNeutralBgSoft
- bg-neutralBgSoftertext-onNeutralBg
- bg-neutralSofttext-onNeutralBg
- bg-neutraltext-neutralBg
- bg-neutralBoldtext-neutralBg
Implementation
Let's start with the base Tailwind CSS file described in Tailwind's documentation.
/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
We define CSS custom properties for each of our primary and neutral shades.
/* global.css */
:root {
--color-primary-100: theme('colors.blue.100');
--color-primary-200: theme('colors.blue.200');
--color-primary-300: theme('colors.blue.300');
--color-primary-400: theme('colors.blue.400');
--color-primary-500: theme('colors.blue.500');
--color-primary-600: theme('colors.blue.600');
--color-primary-700: theme('colors.blue.700');
--color-primary-800: theme('colors.blue.800');
--color-primary-900: theme('colors.blue.900');
--color-neutral-100: theme('colors.gray.100');
--color-neutral-200: theme('colors.gray.200');
--color-neutral-300: theme('colors.gray.300');
--color-neutral-400: theme('colors.gray.400');
--color-neutral-500: theme('colors.gray.500');
--color-neutral-600: theme('colors.gray.600');
--color-neutral-700: theme('colors.gray.700');
--color-neutral-800: theme('colors.gray.800');
--color-neutral-900: theme('colors.gray.900');
}
@tailwind base;
@tailwind components;
@tailwind utilities;
We'll create theme-light
and theme-dark
classes for light and dark mode
respectively.
/* global.css */
@tailwind base;
.theme-light {
}
.theme-dark {
}
@tailwind components;
@tailwind utilities;
We'll start with the light theme, defining all of the variants.
/* global.css */
.theme-light {
--color-primaryBg: var(--color-primary-100);
--color-primaryBgSoft: var(--color-primary-200);
--color-primaryBgSofter: var(--color-primary-300);
--color-primarySoft: var(--color-primary-400);
--color-primary: var(--color-primary-500);
--color-primaryBold: var(--color-primary-600);
--color-onPrimaryBgSofter: var(--color-primary-700);
--color-onPrimaryBgSoft: var(--color-primary-800);
--color-onPrimaryBg: var(--color-primary-900);
--color-neutralBg: var(--color-neutral-100);
--color-neutralBgSoft: var(--color-neutral-200);
--color-neutralBgSofter: var(--color-neutral-300);
--color-neutralSoft: var(--color-neutral-400);
--color-neutral: var(--color-neutral-500);
--color-neutralBold: var(--color-neutral-600);
--color-onNeutralBgSofter: var(--color-neutral-700);
--color-onNeutralBgSoft: var(--color-neutral-800);
--color-onNeutralBg: var(--color-neutral-900);
}
We do the same thing for dark mode.
/* global.css */
.theme-dark {
--color-onPrimaryBg: var(--color-primary-100);
--color-onPrimaryBgSoft: var(--color-primary-300);
--color-onPrimaryBgSofter: var(--color-primary-400);
--color-primaryBold: var(--color-primary-600);
--color-primary: var(--color-primary-800);
--color-primarySoft: var(--color-primary-900);
--color-primaryBgSofter: var(--color-primary-900);
--color-primaryBgSoft: var(--color-primary-900);
--color-primaryBg: var(--color-primary-900);
--color-neutralBg: var(--color-neutral-800);
--color-neutralBgSoft: var(--color-neutral-700);
--color-neutralBgSofter: var(--color-neutral-600);
--color-neutralSoft: var(--color-neutral-500);
--color-neutral: var(--color-neutral-400);
--color-neutralBold: var(--color-neutral-300);
--color-onNeutralBgSofter: var(--color-neutral-200);
--color-onNeutralBgSoft: var(--color-neutral-100);
--color-onNeutralBg: var(--color-neutral-100);
}
We can now hook the custom colors into our Tailwind CSS config.
// tailwind.config.js
module.exports = {
purge: [],
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
primarySoft: 'var(--color-primarySoft)',
primaryBold: 'var(--color-primaryBold)',
primaryBg: 'var(--color-primaryBg)',
primaryBgSoft: 'var(--color-primaryBgSoft)',
primaryBgSofter: 'var(--color-primaryBgSofter)',
onPrimaryBg: 'var(--color-onPrimaryBg)',
onPrimaryBgSoft: 'var(--color-onPrimaryBgSoft)',
onPrimaryBgSofter: 'var(--color-onPrimaryBgSofter)',
neutralBg: 'var(--color-neutralBg)',
neutralBgSoft: 'var(--color-neutralBgSoft)',
neutralBgSofter: 'var(--color-neutralBgSofter)',
neutral: 'var(--color-neutral)',
neutralBold: 'var(--color-neutralBold)',
neutralSoft: 'var(--color-neutralSoft)',
onNeutralBg: 'var(--color-onNeutralBg)',
onNeutralBgSoft: 'var(--color-onNeutralBgSoft)',
onNeutralBgSofter: 'var(--color-onNeutralBgSofter)',
},
},
},
variants: {},
plugins: [],
};
We're ready to start using the semantic colors in our React app.
Let's set the theme class on the top-level HTML element. With React, we'll do
this with a Layout
component that wraps our entire app.
We'll use React's useState
API to tell React the current theme.
// layout.js
import React, { useState } from 'react';
export default function Layout() {
const [lightTheme, setLightTheme] = useState(true);
return (
<div className={lightTheme ? 'theme-light' : 'theme-dark'}>
{/* The rest of the app goes in here. */}
</div>
);
}
We'll create a checkbox to let the user toggle between light and dark mode.
// layout.js
import React, { useState } from 'react';
export default function Layout() {
const [lightTheme, setLightTheme] = useState(true);
return (
<div className={lightTheme ? 'theme-light' : 'theme-dark'}>
<input
type="checkbox"
checked={lightTheme}
onChange={() => setLightTheme(!lightTheme)}
/>
{/* The rest of the app goes in here. */}
</div>
);
}
The fun doesn't have to stop at just light and dark mode either.
Since we've defined our colors semantically, we can have as many themes as we want.
Let's give our users the option to switch between multiple primary colors.
/* global.css */
:root {
--color-neutral-100: theme('colors.gray.100');
--color-neutral-200: theme('colors.gray.200');
--color-neutral-300: theme('colors.gray.300');
--color-neutral-400: theme('colors.gray.400');
--color-neutral-500: theme('colors.gray.500');
--color-neutral-600: theme('colors.gray.600');
--color-neutral-700: theme('colors.gray.700');
--color-neutral-800: theme('colors.gray.800');
--color-neutral-900: theme('colors.gray.900');
}
.theme-blue {
--color-primary-100: theme('colors.blue.100');
--color-primary-200: theme('colors.blue.200');
--color-primary-300: theme('colors.blue.300');
--color-primary-400: theme('colors.blue.400');
--color-primary-500: theme('colors.blue.500');
--color-primary-600: theme('colors.blue.600');
--color-primary-700: theme('colors.blue.700');
--color-primary-800: theme('colors.blue.800');
--color-primary-900: theme('colors.blue.900');
}
.theme-green {
--color-primary-100: theme('colors.green.100');
--color-primary-200: theme('colors.green.200');
--color-primary-300: theme('colors.green.300');
--color-primary-400: theme('colors.green.400');
--color-primary-500: theme('colors.green.500');
--color-primary-600: theme('colors.green.600');
--color-primary-700: theme('colors.green.700');
--color-primary-800: theme('colors.green.800');
--color-primary-900: theme('colors.green.900');
}
.theme-red {
--color-primary-100: theme('colors.red.100');
--color-primary-200: theme('colors.red.200');
--color-primary-300: theme('colors.red.300');
--color-primary-400: theme('colors.red.400');
--color-primary-500: theme('colors.red.500');
--color-primary-600: theme('colors.red.600');
--color-primary-700: theme('colors.red.700');
--color-primary-800: theme('colors.red.800');
--color-primary-900: theme('colors.red.900');
}
I'll leave it as an exercise to the reader to figure out the JavaScript to handle switching between multiple themes.
Hint
The approach is similar to how we implemented toggling between light and dark mode.
Now, we can build our app using the semantic colors we defined.
// title.js
import React from 'react';
export default function Title() {
return <h1 className="text-onNeutralBgSoft ...">Learn React</h1>;
}
The theme is controlled by the classes we apply to the Layout
component.
Recap
In this article, we looked at:
- How to create a semantic color system with Tailwind CSS.
- How to create multiple themes with the help of CSS custom properties.
What are your favorite tricks for theming with CSS?
Let me know on Twitter!
- Author
- Sean Keever
- Last modified
- Sep 19, 2020
- Time to read
- 2 min read