Drew BarontiniProduct Director at Differential

Implementing Dark Mode

Published

January 02, 2020

Reading Time

1 min.

I'm a big fan of Dark Mode. I use all the dark variations of applications (where possible), and I wanted to bring Dark Mode to my site. Here's how I implemented it.

Styles setup

First, let's talk about how I have my styles structured in my codebase.

Managing colors in the theme file

const theme = {
  // ...
  colors: {
    light: {
      // ...
    },
    dark: {
      // ...
    },
  },
  // ...
}

theme.colors is split into a light and dark version with the same set of keys. I wanted to keep a simple palette, so this is the set I came up with:

  • primary for the primary brand color.
  • bg for the background color.
  • fg for the foreground text color.
  • medium for the medium gray color.
  • border for the border color.
  • light for the lightest gray color.

ModeProvider

I created a ModeProvider component for holding the state of the current mode, as well as allowing it to be updated throughout the codebase by leveraging React Hooks.

import React from 'react'
import PropTypes from 'prop-types'

const ModeStateContext = React.createContext()
const ModeDispatchContext = React.createContext()

const KEY = 'db_mode'
const DEFAULT_MODE = 'light'

function ModeProvider(props = {}) {
  const defaultValue = getDefaultValue()
  const [mode, setMode] = React.useState(defaultValue)

  function getDefaultValue() {
    // We're doing this check because of GatsbyJS's server-side rendering.
    if (typeof window !== 'undefined') {
      return window.localStorage.getItem(KEY) || DEFAULT_MODE
    }
    return DEFAULT_MODE
  }

  React.useEffect(() => {
    // We're doing this check because of GatsbyJS's server-side rendering.
    if (typeof window !== 'undefined') {
      window.localStorage.setItem(KEY, mode)
    }
  }, [mode])

  return (
    <ModeStateContext.Provider value={mode}>
      <ModeDispatchContext.Provider value={setMode}>
        {typeof props.children === 'function'
          ? props.children(mode)
          : props.children}
      </ModeDispatchContext.Provider>
    </ModeStateContext.Provider>
  )
}

// const state = useModeState();
function useModeState() {
  const context = React.useContext(ModeStateContext)
  if (context === undefined) {
    throw new Error(`useModeState must be used within a Provider`)
  }
  return context
}

// const setState = useModeDispatch();
function useModeDispatch() {
  const context = React.useContext(ModeDispatchContext)
  if (context === undefined) {
    throw new Error(`useModeDispatch must be used within a Provider`)
  }
  return context
}

// const [state, setState] = useMode();
function useMode() {
  const context = [useModeState(), useModeDispatch()]
  if (context === undefined) {
    throw new Error(`useMode must be used within a Provider`)
  }
  return context
}

ModeProvider.propTypes = {
  children: PropTypes.oneOfType([
    PropTypes.element,
    PropTypes.node,
    PropTypes.func,
  ]),
}

export { ModeProvider as default, useMode, useModeState, useModeDispatch }

This uses React Context to store mode and setMode that are then passed down to any consumers who call it via useMode(). It also handles storing the currently selected mode in localStorage so users will keep the same setting when they move to different pages (or when they revisit the site).

If you're interested in learning more about using React Context — and building provider components like this one — I highly recommend reading 'How to use React Context effectively' by Kent C. Dodds.

Passing the mode to the theme

In order to get the mode to the ThemeProvider (to set the correct theme color), we use the render-props pattern:

<ModeProvider>
  {mode => ({
    /* ... */
  })}
</ModeProvider>

Now we can use mode to set the appropriate colors in the theme:

<ModeProvider>
  {mode => (
    <ThemeProvider
      theme={{
        ...theme,
        colors: theme.colors[mode],
      }}
    >
      {/* ... */}
    </ThemeProvider>
  )}
</ModeProvider>

We're overwriting theme.colors to only have the single set of colors, which will result in either the light or dark set of colors based on the mode.

Toggling between light and dark

In order to toggle between the modes, we grab our useMode() hook:

function DarkModeToggle(props = {}) {
  const [mode, setMode] = useMode()
  const newMode = mode === 'light' ? 'dark' : 'light'

  function handleModeClick(event) {
    event.preventDefault()
    setMode(newMode)
  }

  return <button onClick={handleModeClick}>{newMode}</button>
}

And that's it!

But what about prefers-color-scheme?

prefers-color-scheme is a CSS media feature for detecting if a user has the dark or light version of their system (OS) set. We can use this to set the mode of our site to match the user's setting. But I opted not to implement this.

So you can decide which version you prefer.