DALL·E-2023-03-19-21.56.39-the-image-shows-a-lamp-in-the-center-left-half-of-the-image-is-dark-and-shady-the-other-half-has-warm-light-hand-drawn-by-van-gough

Light or Dark? How to Add Theming to Your React App with JSS and TypeScript

As light and dark themes seem to become a mandatory feature, it’s a perfect opportunity to set up theming and to showcase another important React feature: Contexts. In this post, we will explore how to set up custom themes to a Typescript React JS application using JSS.

This article assumes you already set up a React app with Typescript and JSS as your style provider. In case you need to kickstart your setup, best have a look on https://create-react-app.dev/docs/adding-typescript/. Additionally you can checkout my other post about setting up JSS Styling React Components Made Easy with JSS: A Comprehensive Guide.

First we define a ThemeMode enum that is used to define the possible values for the themeMode property in the ThemeModeContext object. This enum includes two values, LIGHT and DARK, which will be used to toggle between light and dark themes.

export const ThemeMode = {
  LIGHT: 'light',
  DARK: 'dark',
}

Next we need a ThemeModeContext context object that we will use to pass the current theme mode and a function to set the theme mode to our components. React contexts are used for providing a way to share data between components in a React application without having to pass props down the component tree manually. They allow for a more efficient and flexible way to manage and update shared state.

import { createContext } from 'react'
import { ThemeMode } from '../constants'

export type ThemeModeContextValue = {
  themeMode: string
  setThemeMode: (mode: string) => void
}
export const ThemeModeContext = createContext<ThemeModeContextValue>({
  themeMode: ThemeMode.DARK,
  setThemeMode: () => {},
})

Typescript is amazing but it’s just annoying to provide a proper type every time using the `theme` later in our components e.g. as the parameter of the `createUseStyles` hook. Therefore we do a little trick that makes life easier down the road. First we define the theme object globally in `global.d.ts`.

 declare global {
    namespace Jss {
      export interface Theme {
        palette: {
          background: string
        }
      }
    }
  }
  
  export {}

The theme.ts file defines two theme objects, lightTheme and darkTheme, that store the color palette for each theme. These themes are defined using the DefaultTheme interface from the react-jss package, which we did override in the previous step. This allows us to apply to our components using the ThemeProvider component.

import { DefaultTheme } from 'react-jss'

export const darkTheme: DefaultTheme = {
  palette: {
    background: '#1d1d1d',
  }
}

export const lightTheme: DefaultTheme = {
  palette: {
    background: '#f5f5f5',
  },
}

The App.tsx file is where we bring everything together. The current theme mode is passed to the ThemeProvider component based on theme mode, which is the value stored in localStorage or the default value of DARK. The ThemeModeContext.Provider component wraps the entire application and provides access to the ThemeModeContext context object. The handleSetThemeMode function is used to update the themeMode property in the context object when the user switches between themes.

import { useState } from 'react'
import { ThemeProvider } from 'react-jss'
import { lightTheme, darkTheme } from 'utils/theming/theme'
import { ThemeMode } from 'utils/theming/constants'
import { ThemeModeContext } from 'utils/theming/ThemeModeContext/Context'
import useLocalStorage from 'utils/hooks/useLocalStorage'
import MainLayout from './containers/MainLayout'

function App() {
  const [initialThemeMode, setStorageThemeMode] = useLocalStorage<string>(
    'themeMode',
    ThemeMode.DARK
  )
  const [themeMode, setThemeMode] = useState(initialThemeMode)

  const handleSetThemeMode = (newValue: string) => {
    setThemeMode(newValue)
    setStorageThemeMode(newValue)
  }

  return (
    <ThemeModeContext.Provider
      value={{ themeMode, setThemeMode: handleSetThemeMode }}
    >
      <ThemeProvider
        theme={themeMode === ThemeMode.LIGHT ? lightTheme : darkTheme}
      >
        <MainLayout />
      </ThemeProvider>
    </ThemeModeContext.Provider>
  )
}

export default App

Finally, we have the MainLayout uses the createUseStyles hook from the react-jss package to define the styles for the container class. The background color for this class is defined using the palette object from the theme. The useStyles hook is used to get a classes object that is then used to apply the styles to the container div.

import { createUseStyles } from 'react-jss'

const useStyles = createUseStyles(({ palette }) => ({
  container: {
    backgroundColor: palette.background
  },
}))

/**
 * MainLayout
 */
const MainLayout = () => {
  const classes = useStyles()
  return (
    <div className={classes.container}>
      CONTENT
    </div>
  )
}

export default MainLayout

To allow users to set their preferred theme mode you can provide a button on the UI of your website. Now the React context comes in handy, as a ThemeModeContext consumer can now be used to switch between themes from within your application, no matter how deep in the component tree.

import { useContext } from 'react'
import { ThemeModeContext } from 'utils/theming/ThemeModeContext/Context'
...
const { themeMode, setThemeMode } = useContext(ThemeModeContext)
const handleClick = () => {
    // toogle the theme mode
    setThemeMode(
      themeMode === ThemeMode.DARK ? ThemeMode.LIGHT : ThemeMode.DARK
    )
  }
...
<button
    onClick={handleClick}
>Switch to {themeMode === ThemeMode.DARK ? 'LIGHT' : 'DARK'} mode
</button

I hope you find this article and the provided code helpful in implementing theming in your React JS application.


Posted