How to Add Dark Mode to Your React App 🌗

REACTSASSFRONTEND DEVELOPMENTUI RESOURCESREACT DARK MODE

Hey there, fellow developers! 🖥️

Have you ever found yourself loving the sleekness of dark mode? It's that cool, suave look that we often enjoy on code editors, social media platforms, and pretty much any app that offers this option. Even the newest Meta app, Thread, has hopped on the dark mode bandwagon! 🌙

Some people think it’s hard to implement dark mode, but it’s actually easy! Let’s see how to do it in React!

Here are the ingredients:

  • Your React App
  • SASS (not mandatory but suggested as it makes your life a lot easier)
  • React Context.

Giving Your App That Dark Magic ✨

Alright, before we get into complex coding acrobatics, let's start simple: let's design your dark theme.

No need to dive deep into JavaScript and class toggling yet. First things first, make sure you've got your dark theme down pat. Create a fresh CSS class, let's call it theme__dark. Remember, the name isn't set in stone – what matters is using it right. Slap this class onto your app's tag or the highest-level parent element you can reach. In my case, I'm using NextJS, so I created a

in my _app.tsx file and added the class there. If you're working with Vanilla React, it could be in index.tsx (or main.tsx if you're vibing with Vite). This is where you'd normally spot your StrictMode and .createRoot() function.

//_app.*sx, index.*sx or main.*sx
<div className="theme__dark" >
	<Component {...pageProps} />
{/*If you are using Vanilla React, you will find your <App/> here. */}
</div>

Now, let's put that class to work. Make gradual tweaks – start with background color and text color changes. One step at a time, my friend! For example, go for a darker background and lighter text.

If you're a SASS lover, your code might look like this:

.theme__dark {
  background-image: url("../public/assets/bg--dark.png");
  transition: 0.5s;
  * {
    color: white !important;
    div[class*="tags"] span {
      background-color: rgb(153, 0, 255) !important;
    }
  }
  #showcase article {
      background-color: rgb(212, 148, 255, 0.187);
    }
  
  #footer,
  nav {
    background-color: rgb(212, 148, 255, 0.187);
  }
  #shadow * {
    color: $text !important;
  }
}

Or if you're dancing with CSS:

.theme__dark {
  background-image: url("../public/assets/bg--dark.png");
  transition: 0.5s;
}
 .theme__dark * {
  color: white !important;
}
.theme__dark div[class*="tags"] span {
  background-color: rgb(153, 0, 255) !important;
}
.theme__dark #showcase article {
	background-color: rgb(212, 148, 255, 0.187);
}
.theme__dark #footer, .theme__dark nav {
	background-color: rgb(212, 148, 255, 0.187);
}
.theme__dark #shadow * {
	color: $text !important;
}

Remember, simple is chic! As seen above, I focused on adjusting background and text colors. Quick tip: background-color and color are inherited properties!

Once your dark mode transformation is ready, give it a double-check by removing the class. Everything should snap back to normal. If anything sticks around in dark mode, brush up those selectors! Elements should only shape-shift when they're wrapped in the theme__dark class.

Flipping the Light Switch ⚡

💡Handy Resources:

We're going to create a cozy little folder called context to keep things tidy. In here, we'll import createContext from React. This function takes an initial value as its parameter and returns a Context object that we'll introduce as the Provider later on.

Let's use this function to craft our theme context. It's the single source of truth for our dark mode, meaning all info about its state must come from here!

//context/index.ts
import { createContext } from "react";

export const ThemeContext = createContext(false)

Let’s go back to the file where we applied the dark mode class.

Wrap that previously created

with our context, like so:

<ThemeContext.Provider value={true}>
	<div className="theme__dark">
	  <Component {...pageProps} />
  </div>
</ThemeContext.Provider>

Hold your horses – don't worry about the value just yet. Next up, let's tackle the art of toggling this value.

To work our magic, we'll create a component, let's call it "Switch." This component will effortlessly switch between day and night modes with a single click. Don't worry if that sounds too fancy – a basic button will do the trick. The polished look? Save that for later!

Inside this component, we need a way to turn true into false. Here's where our second Context enters the scene.

In the same file as our first context, it's time to introduce the second one: ThemeDispatchContext. This context brings along a dispatch function that lets us update the dark mode value from true to false for all app components.

Now, here's the secret sauce:

import { createContext } from "react";

export const ThemeContext = createContext(false)
//typescript: 
export const ThemeDispatchContext = createContext<React.Dispatch<{ type: string }>>(() => null);
//javascript: 
//export const ThemeDispatchContext = createContext(() => null);

Make sure you type createContext correctly in TypeScript, or it might raise an eyebrow later.

We're almost there, I promise!

Now, let's whip up a reducer function. This function is our go-to magician – it'll actually toggle our value. We'll toss this function into the useReducer hook, so it needs to follow a specific syntax: (value, action: { type: string }) => void.

The first part's the current value, while the second is the kind of action we'll serve up. If you've mingled with Redux before, this pattern should ring a bell.

We’re going to create just one type of action: 'toggle'. I've opted for a switch case, but an if statement's just as cool.

Here's our reducer function:

export function themeReducer(theme: boolean, action: { type: string }) {
    switch (action.type) {
        case 'toggle': return !theme
        default: {
            throw Error('Unknown action');
        }
    }
}

Let’s go apply this reducer.

Back on the file where we applied our first context, let’s apply the second context and create our toggling logic.

Back in the place where we created our first context, let’s wrap the

in our second context.

<ThemeContext.Provider value={true}> //we are about to change this value!!
        <ThemeDispatchContext.Provider value={????????}> //what goes in here? 
          <div className="theme__dark" >
            <Component {...pageProps} />
          </div>
        </ThemeDispatchContext.Provider>
</ThemeContext.Provider>

Let's bring this to life with the useReducer hook. It takes two things: the reducer function we whipped up earlier and the starting value for our dark mode.

That starting value is what the first parameter of our themeReducer function will be.

const [themeRed, dispatch] = useReducer(themeReducer, false)

Now, behold the final masterpiece, complete with a touch of class conditional enchantment:

import { useReducer } from "react";
import "../styles/app.scss"
import { ThemeContext, ThemeDispatchContext, themeReducer } from "@/context";
export default function App({ Component, pageProps }: any) {
  const [themeRed, dispatch] = useReducer(themeReducer, false)
  return (
      <ThemeContext.Provider value={themeRed}>  {/*we replaced true with the value from the reducer.*/} 
        <ThemeDispatchContext.Provider value={dispatch}> {/* we finally gave a value to this other context. */}
          <div className={themeRed ? "theme__dark" : undefined} > {/* if themeRed === true, apply the class 'theme__dark', otherwise don't apply any classes. */}
            <Component {...pageProps} />
          </div>
        </ThemeDispatchContext.Provider>
      </ThemeContext.Provider>
    }
  )
}

With that checked off, let's dive into Switch component magic.

Using the useContext hook, pass the ThemeDispatchContext object as a parameter. Don't forget to do the same for ThemeContext.

//Switch.tsx
const dispatch = useContext(ThemeDispatchContext)
const theme = useContext(ThemeContext)

Ready for the grand finale? When you click, dispatch the 'toggle' action:

<div onClick={() => dispatch({ type: "toggle" })}></div>

And voilà! That's a wrap on our grand dark mode adventure! ✨

//Switch.tsx
import { useContext } from "react"
import { ThemeContext, ThemeDispatchContext } from "@/context"
const Switch = () => {
    const dispatch = useContext(ThemeDispatchContext)
    const theme = useContext(ThemeContext)

    return <div onClick={() => dispatch({ type: "toggle" })}></div>
}

export default Switch
import { createContext } from "react";

export const ThemeContext = createContext(false)
export const ThemeDispatchContext = createContext<React.Dispatch<{ type: string }>>(() => null);
export function themeReducer(theme: boolean, action: { type: string }) {
    switch (action.type) {
        case 'toggle': return !theme
        default: {
            throw Error('Unknown action');
        }
    }
}
// _app.tsx, main.tsx or index.tsx
import { useContext, useReducer } from "react";
import "../styles/app.scss"
import { ThemeContext, ThemeDispatchContext, themeReducer } from "@/context";
export default function App({ Component, pageProps }: any) {
  const [themeRed, dispatch] = useReducer(themeReducer, false)
  return 
      <ThemeContext.Provider value={themeRed}>
        <ThemeDispatchContext.Provider value={dispatch}>
          <div className={themeRed ? "theme__dark" : undefined} >
            <Component {...pageProps} />
          </div>
        </ThemeDispatchContext.Provider>
      </ThemeContext.Provider>
}