Creating dynamic themes with React & TailwindCSS
Tags: react, javascript, css, typescript
Published: 2020-04-19 12:58:14

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](https://tailwindcss.com/) 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](#chapter-1)
  * [Adding and configuring TailwindCSS](#chapter-2)
  * [Setting up our themes](#chapter-3)
  * [Getting Tailwind to use our theme](#chapter-4)
  * [Implementing our theme switcher](#chapter-5)
  * [Conclusions](#chapter-6)

<br>

### Project Setup <a name="chapter-1"></a>

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
```

> 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 <a name="chapter-2"></a>

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

```
yarn add tailwindcss -D
```

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
```

We're going to add tailwind as a [PostCSS](https://github.com/postcss/postcss) plugin, and also add `autoprefixer` to parse our CSS and add vendor prefixes to CSS rules using values from [Can I Use](https://caniuse.com/). 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
```

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

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

module.exports = {
  plugins: [
    require('postcss-import'),
    tailwindcss('./tailwind.js'),
    require('autoprefixer'),
  ],
};
```
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.


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

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

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

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.

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

> 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.

```html
// src/App.tsx
<div className="App bg-red-900">
```
![background-colour-react](https://dev-to-uploads.s3.amazonaws.com/i/ba7e5ttawddejv0wsfw3.png)

Et Voilà!

### Setting up our Themes <a name="chapter-3"></a>

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.

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

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.

```javascript
// 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 || '',
  };
};
```

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`.

```javascript
// 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,
};
```

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.

```javascript
// 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]);
  });
};
```
> 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.

```javascript
// src/themes/utils.ts

...

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

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

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

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

```

### Getting Tailwind to use our theme <a name="chapter-4"></a>

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.

```javascript
// 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: [],
};
```

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](https://reactjs.org/docs/hooks-effect.html) 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.

```javascript
// 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;
```
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](https://dev-to-uploads.s3.amazonaws.com/i/g0kglxb81yrtgipjpbze.png)

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

```html
// src/App.tsx
<div className="App bg-primary-background">
```
![background-app-primary](https://dev-to-uploads.s3.amazonaws.com/i/hlp63zvyiq7fiihdj8am.png)

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 <a name="chapter-5"></a>

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`.

```javascript
// 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>
  );
};
```

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.

```javascript
// 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;
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cuc5o0fsy1nz4d0e6hcg.gif)

### Conclusions <a name="chapter-6"></a>

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