css-modules theme provider

For a recently started React project I chose for css-modules over Styled Components.
It is not so much whether one is better than the other, but which one suits the purpose of your project better. Where CSS Modules is a CSS-preprocessor and Styled Components a Javascript Framework Component. Can’t decide ? I found this a good 11 minute read to make my decision

A requirement for this project is that it can easily, preferably dynamically, alter color patterns. Initially, switching from a dark theme to a light theme will suffice. But the possibility to switch between multiple color schemes would be nice.

I chose for css-modules, and therefore my DuckDuck.go search for a convenient package began. I found some packages, and solutions. But in summary, I felt the solution should be easier. Also, I would like to be able to insert the theming solution into an existing project without too much effort. So here it goes…

Consider the following React component which shows a very fancy text with a apple green background.

import React from "react"
import styles from "./Mum.module.css"

export const Mum = (): JSX.Element => {
  return <div className={styles.hello}>Hi mum!</div>
}
.hello {
  background: #8DB600;
}

I would be nice if the background color of the component could be changed dynamically. E.g. by using a css variable for the background and changing the value on when switching to another theme.

.hello {
  background: var(--theme_bg_color);
}

Styles imported from a CSS module are and object and can be handed over to a child component. So what if we create a parent component with a CSS module containing the CSS variable declaration and give the imported style as property to the child component?

import React from "react"
import styles from "./Grandma.module.css"
import { Mum } from "./Mum" 

export const Grandma = (): JSX.Element => {
  return <div className={styles.applegreen}><Mum /></div>
}
.applegreen {
  --theme_bg_color: #8DB600;
}

That seems to work. When you import styles from a CSS module, those styles become an object. Each CSS class is converted into a key and a value where the key is the same as the CSS class name. Therefore, by declaring multiple CSS classes, imported styles will have a key for each CSS class. The value assigned to each key will include a reference to the CSS style definitions.

import React from "react"
import styles from "./StyleImport.module.css"

export const StyleImport = (): JSX.Element => {
  console.log(JSON.stringify(styles))
  // {"theme--a":"kHMUGg2aJZgighTlz9PX","theme--b":"I2tBZdFDTY6Epwwr3iGA","theme--c":"YFZPxGX36cXYyAR0I75m"}
  return <div>Imported styles</div>
}
.theme--a {
    background: green;
}

.theme--b {
    background: red;
}

.theme--c {
    background: blue;
}

Let’s get back to the wrapper component, Grandma.tsx. The corresponding CSS-module file has been changed and now contains two CSS classes each with the same CSS variable but a different value. The wrapper component now uses the state themeColor which is initialised with the styles from class .themegreen. After 2 seconds the value is set to the styles from class .themered.

import React, {useState} from "react"
import styles from "./Demo.module.css"
import {Mum} from "./Mum"

export const Grandma = (): JSX.Element => {
  const [themeColor, setThemeColor] = useState(styles.themegreen)

  setTimeout(() => {
    setThemeColor(styles.themered)
  }, 2000)

  return (
    <div className={themeColor}>
      <Mum />
    </div>
  )
}
.themegreen {
  --theme_bg_color: #8DB600;
}

.themered {
  --theme_bg_color: #AA4069;
}

And when executing Grandma.tsx the colour of the child component Mum.tsx changes.

To change the ‘Theme’ for this ‘React application’ the only requirement is that CSS modules do use CSS variables. Now put the theme change concept shown in the example above in a React Context so the context can be changed anywhere within the application

import React, {createContext, useState} from "react"
import themes from "../themes/Themes.module.css"

const validThemes = Object.keys(themes)
const defaultTheme = "light"

type ThemeContext = {
  activeTheme: string
  changeTheme: (theme: string) => void
}

const defaultThemeContext: ThemeContext = {
  activeTheme: defaultTheme,
  changeTheme: () => undefined
}

export const ThemeContext = createContext<ThemeContext>(defaultThemeContext)

export const ThemeProvider: React.FC = (props) => {
  const [theme, setTheme] = useState(defaultThemeContext.activeTheme)

  const changeTheme = (themeName: string): void => {
    if (validThemes.includes(themeName)) {
      setTheme(themeName)
    } else {
      setTheme(defaultTheme)
    }
  }

  const themeContext = {
    activeTheme: theme,
    changeTheme: changeTheme
  }

  return (
    <ThemeContext.Provider value={themeContext}>
      <div className={themes[theme]}>{props.children}</div>
    </ThemeContext.Provider>
  )
}
.dark {
  --theme_color: #bbbbbb;
  --theme_font_size: 16px;
  --theme_btn_hover_transition-duration: 0.1s;
}

.light {
  --theme_color: #333333;
  --theme_font_size: 16px;
  --theme_btn_hover_transition-duration: 0.1s;
}

.high-contrast {
  --theme_color: #111111;
  --theme_font_size: 24px;
  --theme_btn_transition-duration: 0.4s;
}

/* https://davidmathlogic.com/colorblind */
.colorblind {
  --theme_bg_color: #ffffff;
  --theme_color: #333333;
  --theme_btn_transition-duration: 0.4s;
}

And then I might look like this

The demo project can be found on

Leave a Reply

20 + 18 =