Creating dynamic themes with React & TailwindCSS

ohitslaurence

Laurence Davies

Posted on April 19, 2020

Creating dynamic themes with React & TailwindCSS

Adding themes is not usually the first thing you'd think about when starting a new project, but what if it was really simple to set up? Imagine living in a world where you were limited to the default VSCode or Slack colour scheme 😱

We're going to look at a way of creating your own dynamic themes using the awesome CSS Variables, which we will be able to update dynamically from within our app to change our whole colour scheme. I'm using TailwindCSS as a css framework here, as it provides everything you need to easily build beautiful, custom user interfaces. Just know that CSS variables are a universal web concept, and you can apply the theming techniques in this article to any javascript project, not just with react and tailwind.

Table Of Contents

Project Setup

We are going to make use of create-react-app as an easy starting point for our themed app. Just run the npx command below to get your project going.



npx create-react-app my-themed-react-app --template typescript


Enter fullscreen mode Exit fullscreen mode

Note: We are going using react with typescript in this tutorial. Feel free to go with the standard template, it won't make too much difference in what we're trying to cover here.

Adding and configuring TailwindCSS

Now, we're going to add tailwind as a devDependency to our project. Run the below code:



yarn add tailwindcss -D


Enter fullscreen mode Exit fullscreen mode

Then we're going to generate a config file to allow us to customise our tailwind installation. This step is important as this config file will act as a bridge between our theme colours and our tailwind classes.



npx tailwind init tailwind.js


Enter fullscreen mode Exit fullscreen mode

We're going to add tailwind as a PostCSS plugin, and also add autoprefixer to parse our CSS and add vendor prefixes to CSS rules using values from Can I Use. We'll also add the postcss-import plugin, to allow us to break up our css across multiple files.



yarn add postcss-cli autoprefixer postcss-import -D


Enter fullscreen mode Exit fullscreen mode

Then we configure PostCSS by creating a postcss.config.js file in our root directory:



// postcss.config.js
const tailwindcss = require('tailwindcss');

module.exports = {
  plugins: [
    require('postcss-import'),
    tailwindcss('./tailwind.js'),
    require('autoprefixer'),
  ],
};


Enter fullscreen mode Exit fullscreen mode

Now here is where things get interesting. We are going to have PostCSS process our css and generate a new css file. This new auto-generated file will have all our app's css, as well as all the tailwind classes.

So how we are going to do this is:

  1. We are going to move the current src/App.css to a new directory: src/css/app.css.
  2. We will have PostCSS read in src/css/app.css and output a new css file to the original src/App.css.
  3. We will create a new css file for our tailwind imports, and import that file into src/css/app.css.
  4. We will create a script to run PostCSS before our app starts.
  5. For good measure, we will add src/App.css to .gitignore, as it will be recreated every time we run the project.


/* src/css/tailwind.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';


Enter fullscreen mode Exit fullscreen mode

And then at the top of src/css/app.css:



/* src/css/app.css */
@import './tailwind.css';


Enter fullscreen mode Exit fullscreen mode

Now we'll add a script to our package.json to run before we start our app. This will tell PostCSS to generate the css file to be used by our app.



  "scripts": {
    ...,
    "prestart": "postcss src/css/app.css -o src/App.css"
  },


Enter fullscreen mode Exit fullscreen mode

Be sure that your main App.tsx file is importing the auto-generated css src/App.css. If you followed all the naming conventions, it should be doing so by default.

And that's it! Now when we start our app, we will be able to make us of all the tailwind goodies. Let's test it out by adding a tailwind class to change the background colour of the default app screen.



// src/App.tsx
<div className="App bg-red-900">


Enter fullscreen mode Exit fullscreen mode

background-colour-react

Et Voilà!

Setting up our Themes

I just want to take a second to think about what we are trying to achieve here. We want to create a theme, configured in a central location and applied across the whole app. We want to be able to create many different themes, and dynamically apply them. As a bonus, it would be great to be able to extend an existing theme (For example, to create a Dark Mode).

So I'm going to start off by creating a new folder src/themes, and in it create a file called base.ts. In this file I'm going to store some variables for our theme.



// src/themes/base.ts
export default {
  primary: '#61DAFB',
  secondary: '#254E70',
  negative: '#e45b78',
  positive: '#A3D9B1',
  textPrimary: '#333',
  backgroundPrimary: '#efefef',
  backgroundSecondary: '#F6F9FC',
};


Enter fullscreen mode Exit fullscreen mode

Now we are going to need a way to take these variables, and map them to css variables to be used by our app. Let's create a new file in the src/themes called utils.ts. Here we will create a function to map our theme variables.



// src/themes/utils.ts
export interface ITheme {
  [key: string]: string;
}

export interface IThemes {
  [key: string]: ITheme;
}

export interface IMappedTheme {
  [key: string]: string | null;
}

export const mapTheme = (variables: ITheme): IMappedTheme => {
  return {
    '--color-primary': variables.primary || '',
    '--color-secondary': variables.secondary || '',
    '--color-positive': variables.positive || '',
    '--color-negative': variables.negative || '',
    '--color-text-primary': variables.textPrimary || '',
    '--background-primary': variables.backgroundPrimary || '',
    '--background-sec': variables.backgroundSecondary || '',
  };
};


Enter fullscreen mode Exit fullscreen mode

Now we are going to need to create a new function to take this theme, and apply the css variables to the :root element of our document. This function, applyTheme, is going to take the string name of our theme, map the variables, then apply it to the :root element.

First, let's create a way to export all our themes in one place, src/themes/index.ts.



// src/themes/index.ts
import base from './base';
import { IThemes } from './utils';

/**
 * The default theme to load
 */
export const DEFAULT_THEME: string = 'base';

export const themes: IThemes = {
  base,
};


Enter fullscreen mode Exit fullscreen mode

Now we can import the list of themes into our new applyTheme function in utils.ts. This function will take the name of our theme, look for it in our list of exported themes, map the css variables, then loop over the mapped object and apply each style to the :root element.



// src/themes/utils.ts
import { themes } from './index';

...

export const applyTheme = (theme: string): void => {
  const themeObject: IMappedTheme = mapTheme(themes[theme]);
  if (!themeObject) return;

  const root = document.documentElement;

  Object.keys(themeObject).forEach((property) => {
    if (property === 'name') {
      return;
    }

    root.style.setProperty(property, themeObject[property]);
  });
};


Enter fullscreen mode Exit fullscreen mode

If you are wondering, the reason why we map our variables instead of just starting with a mapped object, is so that we can make use of the original javascript theme variables anywhere in our components later on if we need.

Now we can call applyTheme anywhere in our app, and it will dynamically apply our new themes variables. As a finishing touch, let's add a function to utils.ts that will allow us to extend an existing theme, and then create a dark theme that we can switch to.

Our extend function will take an existing theme, and then make use of the ES6 spread operator to clone the existing theme and then override it with any new variables that we pass it.



// src/themes/utils.ts

...

export const extend = (
  extending: ITheme,
  newTheme: ITheme
): ITheme => {
  return { ...extending, ...newTheme };
};


Enter fullscreen mode Exit fullscreen mode

Now we can create our dark theme, and export it.



// src/themes/dark.ts
import { extend } from './utils';
import base from './base';

export default extend(base, {
  backgroundPrimary: '#444444',
  backgroundSecondary: '#7b7b7b',
  textPrimary: '#fff',
});



Enter fullscreen mode Exit fullscreen mode

Getting Tailwind to use our theme

Now we need to tell Tailwind to make use of our css variables, so that when we make use of a tailwind class like text-primary, it uses the colour we supplied in our active theme. Tailwind makes this pretty easy for us to do; all we need is to add the variables that we have created into the root tailwind.js file.



// tailwind.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: 'var(--color-primary)',
        secondary: 'var(--color-secondary)',
        negative: 'var(--color-negative)',
        positive: 'var(--color-positive)',
        'primary-background': 'var(--background-primary)',
        'sec-background': 'var(--background-sec)',
        'primary-text': 'var(--color-text-primary)',
      },
    },
    backgroundColor: (theme) => ({
      ...theme('colors'),
    }),
  },
  variants: {
    backgroundColor: ['active'],
  },
  plugins: [],
};


Enter fullscreen mode Exit fullscreen mode

And that's it! Now we can make use of the tailwind classes, and those classes should make use of our active theme. Let's test it out by changing the background colour of our app to our primary-background colour.

First we need to apply our default theme when the app loads. To do this we will make use of the useEffect hook to run our applyTheme function the very first time the app loads, and every time we change the theme state. We'll create a component state variable to track the active theme, and set the initial value to our default theme.



// src/App.tsx
import React, { useEffect, useState } from 'react';
import { DEFAULT_THEME } from './themes';
import { applyTheme } from './themes/utils';
import logo from './logo.svg';
import './App.css';

function App() {
  const [theme, setTheme ] = useState(DEFAULT_THEME);

  /**
   * Run the applyTheme function every time the theme state changes
   */
  useEffect(() => {
    applyTheme(theme);
  }, [theme]);

  return (
    <div className="App bg-primary-background">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

If we start our app and examine our root element, we should be able to see all the css variables that we set.

Alt Text

Now let's change the background class to use our primary background colour.



// src/App.tsx
<div className="App bg-primary-background">


Enter fullscreen mode Exit fullscreen mode

background-app-primary

Awesome right?! Now we can make use of tailwind's classes with our custom colours, and if the css variables in our document root ever change, so will all the colours across our app.

Implementing our theme switcher

Now that we have our theme set up, let's create a way to switch between different themes. What I'm going to do is create a simple button component that we can use to demonstrate our theme switching. This button will make use of the tailwind colour classes, so we can better see how our theme changes as we click the button. Let's create a Button.tsx component in a new folder src/components.



// src/components/Button.tsx
import React from 'react';

type ButtonProps = {
  children?: React.ReactNode;
  onClick?: () => void;
};

export const Button: React.FunctionComponent<ButtonProps> = ({
  children,
  onClick = () => {},
}: ButtonProps) => {
  const baseClasses: string =
    'border-2 outline-none focus:outline-none normal-case tracking-wide font-semibold rounded shadow-xl text-xs px-4 py-2';

  const colourClasses: string =
    'border-primary active:bg-primary-background text-primary bg-sec-background';

  /**
   * Render the button
   */
  return (
    <button className={`${baseClasses} ${colourClasses}`} type="button" onClick={() => onClick()}>
      {children}
    </button>
  );
};


Enter fullscreen mode Exit fullscreen mode

We can now import our button into our main App.tsx component. Let's use some conditional rendering to show one button for our base theme, and another for our dark theme. In this example we are just going to assume that only these two themes exist. Each button will execute our setTheme function, which which update our state variable and in turn execute the applyTheme function in the useEffect hook.



// src/App.tsx
import React, { useEffect, useState } from 'react';
import { DEFAULT_THEME } from './themes';
import { applyTheme } from './themes/utils';
import { Button } from './components/Button';
import logo from './logo.svg';
import './App.css';

function App() {
  const [theme, setTheme] = useState(DEFAULT_THEME);

  /**
   * Run the applyTheme function every time the theme state changes
   */
  useEffect(() => {
    applyTheme(theme);
  }, [theme]);

  return (
    <div className="App bg-primary-background">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p className="text-primary-text">
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <div className="mt-4">
          {theme === 'base' ? (
            <Button onClick={() => setTheme('dark')}>Apply Dark Theme</Button>
          ) : (
            <Button onClick={() => setTheme('base')}>Apply Light Theme</Button>
          )}
        </div>
      </header>
    </div>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

And now we can finally see our theme switching in action!

Alt Text

Conclusions

And that's a wrap! Thanks for getting to the end, I hope you found the techniques in this article useful. I just want to reiterate that the main concepts that we used here are not specific to react projects or tailwind. You can create your own classes/styles using the css variables that we set in our themes - that's the awesome thing about css variables!

This post got pretty long, so I'm going to follow it up with another one which will cover providing theme context to your whole app using the Context API, optimising your css build for production, handling dynamic css classes and writing unit tests for everything.

Source code: https://github.com/ohitslaurence/react-dynamic-theming

💖 💪 🙅 🚩
ohitslaurence
Laurence Davies

Posted on April 19, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related