Toggle Dark Mode

So you are creating your own website and you decide to have a dark and light theme, there might be a few questions that pop on your mind. How do I switch between them? Do I need to store them in a global state manager like Redux or MobX? How to integrate it with my styling solution?

In this blog post I intend on clarifying how I used Context and useContext to store the current state of this website’s theme toggle.

To achieve our goal we will use styled-components and react. You may switch styled-components to vanilla css and dynamically assign css-variables in order to effectively change your theme, some adjustments are required, but the main Context concept will be the same.

Setting Up

We will start by creating two theme objects:

const darkTheme = {
  isDark: true,
  background: 'black',
  primary: 'lightblue',
  secondary: 'pink',
}

const lightTheme = {
  isDark: false,
  background: 'white',
  primary: 'royalblue',
  secondary: 'palevioletred',
}

Note the isDark property above, that is there so you can easily check if the theme is dark through the theme object, which is provided to every styled-component.

With that done, we will need to check if the user has a saved preference from previous visits and the system’s prefers-color-scheme value. I use localStorage for storing preference, but you can use any type of storage you want.

// We don't parse it because "unset" is also a valid value
const savedIsDarkMode = localStorage.getItem('dark')

// Here we leverage matchMedia API to check if the system prefers dark mode
const prefersDarkMode = matchMedia('(prefers-color-scheme: dark)').matches

With those values in hand, we can get an usable isDarkMode value

// Get usable isDarkMode
const getIsDarkMode = () => {
  // Checks if user has a preference already set and use it
  // If not set, then try to select mode by detecting system preference
  switch (savedIsDarkMode) {
    case 'true':
      return true
    case 'false':
      return false
    default:
      return prefersDarkMode
  }
}

With these values in hand, we are ready to create the react context necessary to store our dark mode state, this can be achieved with the code bellow

export const ThemeModeContext = React.createContext({
  theme: getIsDarkMode() ? darkTheme : lightTheme,
  setIsDarkMode: () => {},
})

The ThemeModeContext can be imported and used with useContext in order to call setIsDarkMode from within the app. We initialized the values merely as a formality, so we know the object shape.

Who Provides the Providers?

We are ready to create the mighty ThemeModeProvider, which will encapsulate all of our application’s code.

export const ThemeModeProvider = ({ children }) => {
  const [isDarkMode, setIsDarkMode] = useState(getIsDarkMode())

  useEffect(() => {
    localStorage.setItem('dark', JSON.stringify(isDarkMode))
  }, [isDarkMode])

  const mode = {
    theme: isDarkMode ? darkTheme : lightTheme,
    setIsDarkMode,
  }

  return (
    <ThemeModeContext.Provider value={mode}>
      <ThemeProvider theme={mode.theme}>{children}</ThemeProvider>
    </ThemeModeContext.Provider>
  )
}

Take a look at the code above, it looks quite simple if you are comfortable with hooks and context, but we are going to dissect the component anyway for those who missed on that.

First we have useState which was initialized with the value from getIsDarkMode(). We do this so the state always has the correct value on initialization. Assuming getIsDarkMode() returns true, we then have:

const [isDarkMode, setIsDarkMode] = useState(getIsDarkMode())
//    [true, (state) => newState]

This is the basics of useState. It takes in an initial value and returns an array with the signature [currentState, updateStateFunction], we can then call updateStateFunction('newValue'), and currentState will be updated to equal 'newValue' on the next render.

Next in line we have useEffect, which is basically a function that says run this whenever any value in the dependency array change.

useEffect(() => {
  localStorage.setItem('dark', JSON.stringify(isDarkMode))
}, [isDarkMode])

What we are saying with the above code is: When isDarkMode changes, set the localStorage value to the new isDarkMode value. This ensures that the user preference is always updated.

Lastly we have the Context.Provider itself. As its name implies, it is used to provide Context.Consumer’s with a value. <ThemeModeContext.Consumer> and useContext(ThemeModeContext) will only have a value if the the provider is present in the tree above the consumer. ThemeProvider is used to enable custom styled-components themes and mostly works like any other context.

<ThemeModeContext.Provider value={mode}>
  <ThemeProvider theme={mode.theme}>{children}</ThemeProvider>
</ThemeModeContext.Provider>

Now that you have a better understanding of what the provider does, you need to encapsulate the root of your application with the ThemeModeProvider component. In a regular create-react-app app, it will be something like this:

ReactDOM.render(
  <ThemeModeProvider>
    <App />
  </ThemeModeProvider>,
  document.getElementById('root')
)

Consuming the Context

In order to actually use the context in one of your components, you can either use Context.Consumer component or useContext hook, I will only show how to use the hook implementation as it is much simpler and more straightforward.

Anywhere you want to be able to change the current theme, be it a button or a toggle like in the top of this page, just call useContext(ThemeModeContext) and you will have access to the setIsDarkMode function.

import React, { useContext } from 'react'
import { ThemeModeContext } from './theme'

export const Buttons = () => {
  const { setIsDarkMode } = useContext(ThemeModeContext)
  const setDark = () => setIsDarkMode(true)
  const setLight = () => setIsDarkMode(false)
  const toggleMode = () => setIsDarkMode((s) => !s)
  return (
    <>
      <button onClick={setDark}>set dark</button>
      <button onClick={setLight}>set light</button>
      <button onClick={toggleMode}>toggle</button>
    </>
  )
}

These are all the parts needed to create a fairly simple dark mode toggle. You can, of course, improve and simplify steps as you see fit. To check an actual usage of the code you can check the Code Sandbox bellow.


Hi, this is my personal website and blog.
Find me on Github or LinkedIn