Switching off the lights - Adding dark mode to your React app

Maxime Heckel
Mar 5 · 6 min read
Photo by Alex Knight on Unsplash

Stuck by the paywall? This story is available for free on my personal blog!

Since the release of macOS Mojave, a lot of people have expressed their love for dark mode and a lot of websites like Twitter, Reddit or Youtube have followed this trend. Why you may ask? I think the following quote from this Reddit post summarizes it pretty well:

Night is dark. Screen is bright. Eyes hurt.
Night is dark. Screen is dark. Eyes not hurt.

As I want to see even more websites have this feature, I started experimenting with an easy a non-intrusive way to add a dark mode to my React projects, and this is what this article is about.
In this post, I’m going to share with you how I built dark mode support for a sample React app with Emotion themes. We’ll use a combination of contexts, hooks, and themes to build this feature and the resulting implementation should not cause any fundamental changes to the app.

Note: I use Emotion as a preference, but you can obviously use CSS modules or even inlines styles to implement a similar feature.

What we’re going to build:

The objective here is to have a functional dark mode on a website with the following features:

  • a switch to be able to enable or disable the dark mode
  • some local storage support to know on load if the dark mode is activated or not
  • a dark and light theme for our styled components to consume

Theme definitions

The theme definitions for our example.

You’ll notice in the code above that I gave very descriptive names to my variables such as background or body. I always try to make sure none of the variables names are based on the color so I can use the same name across the different themes I’m using.

Now that we have both our dark and light theme, we can focus on how we’re going to consume these themes.

Theme Provider

Loading the state in Context

  • a boolean that tells us whether or not the dark theme is activated, defaults to false.
  • a function toggle that will be defined later.

This state will be the default state in a ThemeContext, because we want to have access to these items across our all application. In order to avoid having to wrap any page of our app in a ThemeContext.Consumer, we’ll build a custom useTheme hook based on the useContext hook. Why hooks? I think this tweet summarizes it pretty well:

As it is stated in the tweet above, I really believe that hooks are more readable than render props:

Default state and ThemeContext.

In this ThemeProvider component, we’ll inject both the correct theme and the toggle function to the whole app. Additionally, it will contain the logic to load the proper theme when rendering the app. That logic will be contained within a custom hook: useEffectDarkMode.

Code for the useEffectDarkMode custom hook. It’s based on both useState and useEffect.

In the code above, we take advantage of both the useState and useEffect hook. The useEffectDarkMode Hook will set a local state, which is our theme state when mounting the app. Notice that we pass an empty array [] as the second argument of the useEffect hook. Doing this makes sure that we only call this useEffect when the ThemeProvider component mounts (otherwise it would be called on every render of ThemeProvider).

Code for the ThemeProvider component that provides both theme and themeState to the whole application.

The code snippet above contains the (almost) full implementation of our ThemeProvider:

  • If dark is set to true in localStorage, we update the state to reflect this and the theme that will be passed to our Emotion Theme Provider will be the dark one. As a result, all our styled component using this theme will render in dark mode.
  • Else, we’ll keep the default state which means that the app will render in light mode.

The only missing piece in our implementation is the toggle function. Based on our use case, it will have to do the following things:

  • reverse the theme and update the themeState
  • update the dark key in the localStorage
Code for the toggle function. This function is injected in the ThemeContext and aims to toggle between light and dark mode.

Adding the theme switcher

The next step is to provide a button on the UI to enable or disable the dark mode. Luckily, we have access to all the things we need to do so through the useTheme hook, which will give us access to what we’ve passed to our ThemeContext.Provider in part two of this post.

Sample app wrapped in the ThemeProvider using the useTheme hook.

Considering we’re in the default state (light mode), clicking this button will call the toggle function provided through the ThemeContext which will set the local storage variable dark to true and the themeState dark variable to true. This will switch the theme that is passed in the Emotion Theme Provider from light to dark. As a result, all our styled components using that theme will end up using the dark theme, and thus our entire application is now in dark mode.
In the example above, the Wrapper component uses the colors of the theme for the fonts and the background, when switching from light to dark these CSS properties will change and hence the background will go from gray to black and the font from black to white.


The dark mode implementation on my website (sorry for the low frame rate 😅).

If you want to get a sample project with dark mode to hack on top of it, check out this minimal React app I built with all the code showcased on this article.

If you liked this article don’t forget to hit the “clap” button and if you have any other questions I’m always reachable on Twitter, or on my website. You can also subscribe to my Medium publication to not miss my next post.

Maxime Heckel

Software engineer and space enthusiast. Currently working for @docker.

Maxime Heckel

Written by

Software engineer and space enthusiast. Currently working for @docker.

Maxime Heckel

Software engineer and space enthusiast. Currently working for @docker.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade