Themes in React with Typescript, Context API, React hooks, CSS variables

2021-12-297 min

How to create themes in Typescript React application using Context API, React hooks, and CSS variables

In this article, I go back to the beginning of HelloJS, to the time when I recorded a video about themes in React application. One and a half years after this film was recorded, I still think that many of you might find it useful, so I decided to write an article about it.

Criteria

We are going to build a theme layer within the configuration of the theme colors, and functionality which let us change the theme from one to another.

Tools used in this example:

  • React with Context API and hooks
  • Typescript
  • CSS variables

Example application

themes-in-react-app.png

There you can get the repository

Idea explanation

Context definition and its scope

feature-architecture.png

As you can see in the picture above, we want to define a ThemeContext and then wrap the whole app inside a ThemeContext.Provider.

Thanks to that we will be able to use our custom useTheme hook in every place in the App component structure.

theme-usage.png

Theme configuration

react-themes-configuration.png

Another step is to create good typing and theme configuration.

Implementation

Types definition

First of all, let's define a Theme interface. My idea here is to define a Theme interface which has keys named in CSS variable naming convention. Color names are the same for all themes, but colors values are flexible depending on the current theme. Later you will see how I found it useful.

interface Theme {
  '--primary': Color;
  '--secondary': Color;
  '--background': Color;
}

Next, I created a Color enum which defines all colors used in the app.

enum Color {
  VIOLET = '#9E25FC',
  DARK_VIOLET = '#250341',
  LIGHT_GRAY = '#F4F4F4',
  WHITE = '#FFF',
}

We also need a ThemeType type to determine the selected theme.

type ThemeType = 'dark' | 'light';

In the end, we need a themes config describing all about the colors for each theme.

Themes config

const THEMES: Record<ThemeType, Theme> = {
  light: {
    '--primary': Color.VIOLET,
    '--secondary': Color.DARK_VIOLET,
    '--background': Color.LIGHT_GRAY,
  },
  dark: {
    '--primary': Color.VIOLET,
    '--secondary': Color.WHITE,
    '--background': Color.DARK_VIOLET,
  }
};

A theme config is an object with ThemeType keys and Theme objects as values.

Themes provider

import React, { Dispatch, SetStateAction } from 'react';
import { THEMES } from './Theme.config';
import { ThemeType, Theme } from './Theme.types';

interface ThemeContextProps {
  themeType: ThemeType;
  theme: Theme,
  setCurrentTheme: Dispatch<SetStateAction<ThemeType>> | null
}

export const ThemeContext = React.createContext<ThemeContextProps>({
  themeType: 'light',
  theme: THEMES['light'],
  setCurrentTheme: null
}

export const ThemeProvider: React.FC = ({ children }) => {
  const [currentTheme, setCurrentTheme] = React.useState<ThemeType>('light');

  return (
    <ThemeContext.Provider value={{
      themeType: currentTheme,
      theme: THEMES[currentTheme],
      setCurrentTheme,
    }}>
      {children}
    </ThemeContext.Provider>
  )
}

export const useTheme = () => React.useContext(ThemeContext);

The ThemeContext is a common react context implementation. First I defined a context type ThemeContextProps, then I created context with an initial state

const ThemeContext = React.createContext<ThemeContextProps>({
  themeType: 'light',
  theme: THEMES['light'],
  setCurrentTheme: null
}

The next step is to create a ThemeProvider which takes children as an argument. It's important to be able to render react components inside ThemeProvider.

ThemeProvider manages currentTheme state. In the end, we return ThemeContext.Provider with value object. In this example, I passed themeType, theme, and setCurrentTheme.

  • themeType - To be aware of which theme is the current one and be able to react to that information.
  • theme - This is an object containing all of the current theme colors
  • setCurrentTheme - The function for theme change

Wrapping application into the ThemeProvider

To have a possibility of using useTheme hook in the App component, I decided to wrap the whole react render in ThemeProvider

ReactDOM.render(
  <React.StrictMode>
    <ThemeProvider>
      <App />
    </ThemeProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

Cool, that's all about implementation. Now we can use our theme.

Usage

To use the theme, we can simply get it from useTheme hook.

const { theme, themeType } = useTheme()

Now depends on themeType we can render a dedicated graphic or another part of the UI, like this:

const logo = themeType === 'light' ? darkLogo : lightLogo 

Cherry on top

To use dynamic theme colors, we need to pass the theme object as CSS variables.

<div
  style={{
     ...theme
  } as React.CSSProperties}
>
  // content
</div>

What happened above is basically spreading the theme object in style attribute for a div. Now it's a good time to remind you how the theme object looks like. So theme object contains keys named in CSS variable naming convention, and the values are colors defined in Color enum.

theme-colors-in-the-browser.png

Thanks to that, we passed CSS variables with these values, for example

 '--primary': Color.VIOLET, // '#9E25FC'

Now we can use it in CSS files.

.example {
  color: var(--primary)
}

If you don't know CSS variables, I encourage you to read my article about introduction to CSS variables.

Colors scope

If we pass the theme as a value for the style attribute at the very top level of the app, we will have all of the colors accessible for the whole components tree.

And that's all, I hope you found it useful.

If you need more explanation, please check out my youtube video