Frontend
React theming with CSS variablesReact theming with CSS variables
React theming with CSS variables
Theming in React can be complicated. As the project grows in size, so does the amount of visual variables you have to coordinate, manage, and maintain. Here are a series of tips and tricks that can be used to help keep your CSS under control in a React context.
Theming using CSS variables is extremely fast for rendering. However, the number of variables and associated names can significantly grow as the project size increases.
Here is an example of how to theme your React app based on CSS variables:
body.light { --app-color-primary: blue; --app-color-primary-rgb: 0, 0, 255; --app-color-primary-contrast: white; --app-color-light: white; --app-color-light-rgb: 255, 255, 255; --app-color-light-contrast: black; --app-color-dark: black; --app-color-dark-rgb: 0, 0, 0; --app-color-dark-contrast: white; } body.dark { --app-color-primary: blue; --app-color-primary-rgb: 0, 0, 255; --app-color-primary-contrast: white; --app-color-light: black; --app-color-light-rgb: 0, 0, 0; --app-color-light-contrast: white; --app-color-dark: black; --app-color-dark-rgb: 0, 0, 0; --app-color-dark-contrast: white; }
How can we make this a manageable process? a quick way is to set them up as a React Context.
What is React Context? It is a React API that lets you "pass data through the component tree without having to pass props down manually at every level."
Setting your CSS up in a JavaScript Object for React
Using the above example, we can add them as a context for React via a JavaScript object. Here is an example:
//themes.js const theme = { colors: { primary: { main: 'var(--app-color-primary)', rgb: 'var(--app-color-primary-rgb)', contrast: 'var(--app-color-primary-contrast)', }, light: { main: 'var(--app-color-light)', rgb: 'var(--app-color-light-rgb)', contrast: 'var(--app-color-light-contrast)', }, dark: { main: 'var(--app-color-dark)', rgb: 'var(--app-color-dark-rgb)', contrast: 'var(--app-color-dark-light)', }, }, };
Using your JavaScript object to theme your React app
Here is an example of how to use the above theme configuration as a JavaScript.
import { theme } from './theme'; const useStyles = createUseStyles({ button: { display: 'flex', fontSize: '1rem', backgroundColor: theme.colors.primary.main, }, });
The theme
object lives out of React, so theme switching triggers no re-renders - resulting in a more performant app. It also makes your CSS variables accessible with intelliSense, resulting in easier and better-referenced workflows.
Using the theme switcher hook with Jotai
While the above method makes adding CSS an easier process to your React app, the number of variables you have to maintain can grow to an unmanageable size. Jotai is a highly popular primitive and flexible state management store for React. Jotai can be used to manage localStorage
, switch theme states, and maintain the memory of them for the app.
To install Jotai as a module for your project, you can do so via npm
:
npm i jotai
Here is an example implementation of Jotai.
import { atom, useAtom } from 'jotai'; import { useEffect } from 'react'; const browser = typeof window !== 'undefined'; const localValue = browser ? localStorage.getItem('theme') : 'light'; const systemTheme = browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; // The atom to hold the value goes here const themeAtom = atom(localValue || systemTheme); /** Sitewide theme */ export function useTheme() { const [theme, setTheme] = useAtom(themeAtom); useEffect(() => { if (!browser) return; localStorage.setItem('theme', theme); document.body.classList.remove('light', 'dark'); document.body.classList.add(theme); }, [theme]); return [theme, setTheme]; }
Here is a breakdown of the code above.
- First, we check if the code is running in a browser. This is done through
typeof window !== 'undefined';
- Then we get the value stored in
localStorage
. IflocalStorage
has the theme in it, then we'll consider it the highest priority. Our fallback default islight
. - The
themeAtom
is our main store to hold variables for the current theme. We make this a local state usinguseAtom
:const [theme, setTheme] = useAtom(themeAtom);
Themes can be modified usingsetTheme
. useEffect
integrates the current theme with the CSS.
useEffect
runs whenever theme
changes. When this runs, it checks if the code is running in the browser. If it isn't, it simply stops further execution by putting a return
.
If it is successful, it goes on, and removes all the classes corresponding to our themes on <body>
, then it adds the class corresponding to the latest value of theme
variable.
Finally, we return the [theme, setTheme]
pair as it is, so we can use it just like we use useState
. You could also return these as objects { theme, setTheme }
giving them explicit naming.
Here is the TypeScript version of the above code:
import { atom, useAtom } from 'jotai'; import { useEffect } from 'react'; export type Theme = 'light' | 'dark'; const browser = typeof window !== 'undefined'; const localValue = (browser ? localStorage.getItem('theme') : 'light') as Theme; const systemTheme: Theme = browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; // The atom to hold the value goes here const themeAtom = atom<Theme>(localValue || systemTheme); /** Sitewide theme */ export function useTheme() { const [theme, setTheme] = useAtom(themeAtom); useEffect(() => { if (!browser) return; localStorage.setItem('theme', theme); document.body.classList.remove('light', 'dark'); document.body.classList.add(theme); }, [theme]); return [theme, setTheme] as const; }