Detecting Dark Mode With React Hooks

It feels like practically every website and app supports dark mode these days. The feature has been widely adopted, partially because the traditional CSS implementation is quite simple.

That’s cool for a simple website where all you need to do is override some theme colors, but what if you’re building something more complex? What if JavaScript also needs to know if we’re viewing light or dark mode based on the user’s system preferences?

Luckily you can keep it all in sync with a simple React hook.

The CSS Way

Dark mode is traditionally implemented by using the prefers-color-scheme media query to invert colors when dark mode is active.

.box {
  color: black;
  background: white;
}
@media (prefers-color-scheme: dark) {
  // Invert some colors here
  .box {
    color: white;
    background: black;
  }
}

Using CSS is nice because the media query dynamically changes in response to the appearance mode the user chooses in the system preferences:

Dark mode as controlled by MacOS system preferences.
Dark mode as controlled by MacOS system preferences.

No event listeners or complex stuff... just a media query.

The React Hook Way

Alright... so how do we do this in React? Three simple steps:

  1. Read the initial value of the media query on mount
  2. Add a change event listener to the prefers-color-scheme media query
  3. Store the theme name with useState

This is the full hook! No additional dependencies are required.

import { useEffect, useState } from "react"

export type ThemeName = "light" | "dark"

function useTheme() {
  const [themeName, setThemeName] = useState<ThemeName>("light")
  useEffect(() => {
    // Set initial theme
    if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
      setThemeName("dark")
    } else {
      setThemeName("light")
    }
    // Listen for theme changes
    window
      .matchMedia("(prefers-color-scheme: dark)")
      .addEventListener("change", (event) => {
        if (event.matches) {
          setThemeName("dark")
        } else {
          setThemeName("light")
        }
      })
  }, [])
  return {
    themeName,
    isDarkMode: themeName === "dark",
    isLightMode: themeName === "light",
  }
}

export default useTheme

The hook exports:

  • The current themeName
  • Helpful isDarkMode and isLightMode booleans

Which can be easily consumed by any component:

import useTheme from "@hooks/useTheme"

function Box() {
  const { isDarkMode } = useTheme()
  return (
    <div
      style={{
        color: isDarkMode ? "white" : "black",
        background: isDarkMode ? "black" : "white",
      }}
    >
      This is a box with light and dark mode!
    </div>
  )
}

export default Box

Pretty handy!