Setting Up Themes with Tailwind CSS and React

Last modified
September 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-primaryBg
    text-onPrimaryBgSofter
  • bg-primaryBgSoft
    text-onPrimaryBgSoft
  • bg-primaryBgSofter
    text-onPrimaryBg
  • bg-primarySoft
    text-white
  • bg-primary
    text-white
  • bg-primaryBold
    text-white
  • bg-neutralBg
    text-onNeutralBgSofter
  • bg-neutralBgSoft
    text-onNeutralBgSoft
  • bg-neutralBgSofter
    text-onNeutralBg
  • bg-neutralSoft
    text-onNeutralBg
  • bg-neutral
    text-neutralBg
  • bg-neutralBold
    text-neutralBg

Implementation

Let's start with the base Tailwind CSS file described in Tailwind's documentation.

/* index.css */
@tailwind base;

@tailwind components;

@tailwind utilities;

We define CSS custom properties for each of our primary and neutral shades.

/* index.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.

/* index.css */
@tailwind base;

.theme-light {
}
.theme-dark {
}

@tailwind components;

@tailwind utilities;

We'll start with the light theme, defining all of the variants.

/* index.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.

/* index.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.

/* index.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!

Last modified
September 19, 2020
Time to read
2 min read

Get the latest articles
Sign up for the newsletter

I will not send you spam. Unsubscribe at any time.

Was this helpful?