How to Implement Dark Mode in React Using Context

Dark Mode in React & TailwindCSS

Post cover displaying React logo
Go Dark or Go Home

Dark mode is everywhere nowadays, and probably one of the best things in a website (at least for us, the developers), Together in this post we will learn how to implement dark mode in a React application using TailwindCSS for styles and React's Context API for data passing and theme switching.

🌐
You can find the source code at the end of the post.

Table of Contents

Requirements

  • Basic understanding of React and Typescript

Why Use TailwindCSS?

The main reason I'm using TailwindCSS is it's ease of use, especially when it comes to dark mode.

We will be using the class strategy instead of the media strategy to manually trigger dark mode.

Why Use Context API?

We will be using the Context API for two reasons, the first is to pass data down to whatever child component we want (the one responsible for theme switching) and the second is to update the theme.

Create the Project

Before the darkness, we need a project, the project used in this post is generated with Create React App using the Typescript template.

Run the following command to generate a project:

npx create-react-app react-dark-mode-context --template typescript

Feel free to change the name of the project.

Install Dependencies and Setup

For the project dependencies, we don't have much – it's just TailwindCSS because Context API is a built-in API/Hook in React.

Install TailwindCSS:

npm i -D tailwindcss postcss autoprefixer

And initialize project's configuration:

npx tailwindcss init

Now you will have two files generated, we need to tweak the Tailwind config file to include our src and to specify the dark mode strategy:

The config file should look something like this:

module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
  darkMode: 'class',
}

Now add the Tailwind directives to our root styles file (It's the index.css file) :

@tailwind base;
@tailwind components;
@tailwind utilities;

That's it for the installation and setup, let's move on.

Create Components

For the sake of this post, let's keep things as simple as we can.

For that reason, below you will see we have two components (not including the App component).

So, let's create these components (order doesn't matter, but it will be easier if you follow the below order):

Create a components folder in the src folder, then create a file for each component (except the App.tsx file, it's already there).

The ThemeSwitch component:

import React from 'react'

const ThemeSwitch = () => {
  return (
    <button
      style={{
        padding: 5,
        borderRadius: 5,
        color: 'black',
        background: 'white',
      }}
    >
      Go DARK MODE
    </button>
  )
}

export default ThemeSwitch

The MainComponent component:

import { FC } from 'react'
import logo from '../logo.svg'
import ThemeSwitch from './ThemeSwitch'

const MainComponent: FC = () => {
  return (
    <div className='font-sans flex flex-col justify-center items-center h-screen dark:bg-zinc-700 dark:text-white'>
      <img src={logo} width={200} alt='React Logo' />
      <h1 className='text-2xl'>Hello World!</h1>
      <h2>React + TailwindCSS Dark Mode App</h2>
      <div className='mt-2'>
        <ThemeSwitch />
      </div>
    </div>
  )
}

export default MainComponent

Note that I added some dark mode styles in the component using the dark: keyword, we will see the effect once we implement the theme context and switching.

The App component:

import { FC } from 'react'
import MainComponent from './components/MainComponent'

const App: FC = () => {
  return <MainComponent />
}

export default App

All good, let's implement the theme switching using the Context API.

Create Theme Context and Wrapper

Now we are in the exciting part, the Context API – in this section we will create two files, one for the theme context and the other for the wrapper, you might ask why we need a wrapper? It's just to make the App component as clear and clean as possible.

Create a context folder in the src folder.

The ThemeContext file:

import { createContext } from 'react'

const defaultValue = {
  currentTheme: 'light',
  changeCurrentTheme: (newTheme: 'light' | 'dark') => {},
}

const ThemeContext = createContext(defaultValue)

export default ThemeContext

This file has the context, and it's default value, currentTheme is self-explanatory and the method changeCurrentTheme is the method responsible for theme switching (we will write its implementation/content in the wrapper).

The ThemeContextWrapper component:

import { useState, useEffect, FC, ReactNode } from 'react'
import ThemeContext from './ThemeContext'

const ThemeContextWrapper: FC<{ children: ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState(localStorage.getItem('theme') || 'light')

  const changeCurrentTheme = (newTheme: 'light' | 'dark') => {
    setTheme(newTheme)
    localStorage.setItem('theme', newTheme)
  }

  useEffect(() => {
    if (theme === 'light') document.body.classList.remove('dark')
    else document.body.classList.add('dark')
  }, [theme])

  return <ThemeContext.Provider value={{ currentTheme: theme, changeCurrentTheme }}>{children}</ThemeContext.Provider>
}

export default ThemeContextWrapper

This is the real deal here, the core of operations – let me explain what happens here.

We need to make it clear that Context's values are being accessible for child components if they are wrapper around a provider (what we return in this case), this explains why we have children as props to this component, inside the component we have a simple state for the current theme also the implementation of the previous method we discussed that changes the theme.

At first render, the component checks for the current value stored in local storage (yes, this project is preference-capable, surprise!) if the value is not found then we fall-back to light mode, all this happens in the useState hook at first.

Then based on that state the useEffect is triggered to set the suitable class (which is basically adding pr removing the dark class from the document body, remeber we said we are using the Tailwind class strategy? Well, this is it).

The useEffect will run every time the theme value from the state changes, which happens when the changeCurrentTheme function is called, we update the state's value with the new theme selected and persist this value in local storage.

That's it, it's this simple.

Tweak ThemeSwitch Component

There is one thing we need to change in the ThemeSwitch component, we need to use the ThemeContext values and capabilities.

Open up that component and edit like below:

import React from 'react'
import ThemeContext from '../context/ThemeContext'

const ThemeSwitch = () => {
  const { currentTheme, changeCurrentTheme } = React.useContext(ThemeContext)

  return (
    <button
      data-testid='switch-theme-btn'
      style={{
        padding: 5,
        borderRadius: 5,
        color: currentTheme === 'light' ? 'white' : 'black',
        background: currentTheme === 'light' ? 'black' : 'white',
      }}
      onClick={() => changeCurrentTheme(currentTheme === 'light' ? 'dark' : 'light')}
    >
      Go {currentTheme === 'light' ? 'DARK MODE' : 'LIGHT MODE'}
    </button>
  )
}

export default ThemeSwitch

You can notice that we are accessing the current theme value and the method to switch themes using the Context hook.

The current theme value is used to determine the styles in the component, as well as the value passed to the method when being called after clicking the switching button.

Now, that's it for real, we have a dark mode enabled React application.

Conclusion

As we saw, in simple steps we implemented dark mode in a React app, now it's up to you to change the styles using Tailwind dark: keyword to make the necessary changes for a dark environment.

Live demo

Sources

Source code can be found here.


As always, I hope you learned something.

Found this useful? feel free to share it with your friends.

Join the newsletter from to notify you of new posts and updates.

Like the post? consider buying us a coffee ❤️.