How to Implement Dark Mode in React Using Context
Dark Mode in React & TailwindCSS
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.
Table of Contents
- Requirements
- Why Use TailwindCSS?
- Why Use Context API?
- Create the Project
- Install Dependencies and Setup
- Create Components
- Create Theme Context and Wrapper
- Tweak ThemeSwitch Component
- Conclusion
- Sources
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.
Sources
- Create React App
- React Context
- TailwindCSS installation and dark mode
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 here to notify you of new posts and updates.
Like the post? consider buying us a coffee ❤️.