Dark mode. This design trend has almost become expected by most users with websites and web applications. Dark mode has been possible to integrate with Tailwind projects, but required some workarounds utilizing CSS variables and the config file got messy and hard to read. With Tailwind v2.0, dark mode is built right into the framework and removes the need to work with those pesky CSS variables!
In this article, I'll show you how to get dark mode working in a React application (stick to the end - I'll give you a starter template!). Even though this is done in React, the main ideas apply to all modern JavaScript frameworks.
Let's get started!
TL;DR
I acknowledge this is a long article, so I'll give you a summary.
Tailwind v2.0 gives us the flexibility to choose how we want to implement dark mode. If we want full control, Tailwind will look for an element in the DOM with the dark
class attached to it. If the element is found, elements styled with the dark
variant will be applied.
Here's an example of styling a component with dark mode :
<div class="bg-white dark:bg-gray-800">
<h1 class="text-gray-900 dark:text-white">Dark mode is here!</h1>
<p class="text-gray-600 dark:text-gray-300">
Lorem ipsum...
</p>
</div>
It's up to us to supply the functionality that dynamically adds/removes the dark
class to a top-level document element. In this tutorial, we utilized the Context API to expose a theme variable and a function to change the value. When the value is changed, it triggers a function in a component called ThemeProvider
that adds/removes the dark
class on the root document element.
Want to see the finished code?
Check out my starter template HERE!
Here's a preview of what you get with the template:
Thinking Through the Problem
Still here? Cool!
I prefer to break features down into manageable tasks before I begin to work on them. It helps me understand requirements, identify and pain points and better think through the problem.
The first thing we should figure out is how Tailwind wants developers to work with the new dark mode feature.
According to the documentation, Tailwind v2.0 now includes a dark
variant. This means that you can style your elements as you normally would, and for those elements that you want to alter their appearance in dark mode, you simply add the 'dark' variant in front of the style utilities. You can chain them too, which is pretty neat. Here's a basic example of styling an element with both light and dark mode:
<div class="bg-white dark:bg-gray-800">
<h1 class="text-gray-900 dark:text-white">Dark mode is here!</h1>
<p class="text-gray-600 dark:text-gray-300">
Lorem ipsum...
</p>
</div>
Styling elements with the dark
variant won't actually do anything until dark mode is enabled in the project.
In the tailwind.config.js
file, we can include the darkMode
option to enable the feature in Tailwind.
It's important to note that there are two values we can use here - media
and class
.
The media
value uses the prefers-color-scheme
media feature to detect if the user has dark mode selected as their appearance preference. This is the easiest option and doesn't require any additional set up to get working. Simply set the value to media
, styles your elements with the dark
variant, and you're good to go!
// tailwind.config.js
module.exports = {
darkMode: 'media',
// ...
}
On the other hand, if we want to include a manual appearance switch on the web application, we can use the class
value. Since most websites and applications allow us to switch dark mode on or off (usually with a toggle), this is that value we'll use in our example (and we'll manually use prefers-color-scheme
just for the fun of it!). Because this option doesn't do any of the heavy-lifting for us, it's our job to implement the toggle functionality.
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
Continuing the thought above, in order for the dark mode styles to take effect, Tailwind searches through the DOM for an element with a dark
class applied to it. If it finds that element, all of the dark
variant style utilities will be applied.
Now that we understand how Tailwind wants us to work with dark mode and what we need to do to manually switch the theme, let's generate the project!
Generating the React Project & Installing Tailwind
First things first. We need a project to work in. I won't go into detail on how to generate a React project and how to install Tailwind, instead, I'll link you to the official Tailwind documentation.
Enabling Dark Mode
We've hit on this briefly above, but the next step is to actually enable dark mode in our tailwind.config.js
file. Make sure you set darkMode
to a value of class
.
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
Removing Boilerplate Code
Open the App.js
file and replace with boilerplate code with the following markup.
import './App.css';
function App() {
return (
<div className="px-4 mx-auto max-w-screen-sm md:max-w-screen-md md:p-0 lg:max-w-screen-lg xl:max-w-screen-xl">
<div className="min-h-screen flex justify-center items-center">
<h1 className="text-gray-900 dark:text-white text-3xl sm:text-5xl lg:text-6xl leading-none font-extrabold tracking-tight mb-8">Dark Mode Template</h1>
</div>
</div>
);
}
export default App;
We set some max-width breakpoints on the outer div
and incorporate the dark
variant on the h1
tag so the text color changes to white when dark mode is active.
The ThemeContext Component
We want any part of our application (page, component, etc) to be able to determine the active theme (light or dark). We can use the Context API to achieve this, so let's create a component that encapsulates that logic and exposes the necessary context.
This component will also store the current theme in local storage so the theme is saved for the next time the user visits the app.
Create a new file (I've placed it in a folder named components
) called themeContext.js
and paste the following code.
import React from 'react';
const getInitialTheme = () => {
if (typeof window !== 'undefined' && window.localStorage) {
const storedPrefs = window.localStorage.getItem('color-theme');
if (typeof storedPrefs === 'string') {
return storedPrefs;
}
const userMedia = window.matchMedia('(prefers-color-scheme: dark)');
if (userMedia.matches) {
return 'dark';
}
}
// If you want to use dark theme as the default, return 'dark' instead
return 'light';
};
export const ThemeContext = React.createContext();
export const ThemeProvider = ({ initialTheme, children }) => {
const [theme, setTheme] = React.useState(getInitialTheme);
const rawSetTheme = (rawTheme) => {
const root = window.document.documentElement;
const isDark = rawTheme === 'dark';
root.classList.remove(isDark ? 'light' : 'dark');
root.classList.add(rawTheme);
localStorage.setItem('color-theme', rawTheme);
};
if (initialTheme) {
rawSetTheme(initialTheme);
}
React.useEffect(() => {
rawSetTheme(theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
There's a lot here. Let's break it down.
First, we declare and export a variable which holds some state context. We'll import this variable in the Toggle
component in a few minutes.
export const ThemeContext = React.createContext();
The getInitialTheme()
function looks more complicated than it actually is. This function's job is to check the user's appearance preference, and if they have set it to dark mode, the function returns dark
as the value. If the user has not selected dark
as their default system appearance, this function returns light
. Note that if you want your application to show a dark appearance by default, you can alter this function to return dark
.
This function is being called inside the ThemeProvider
component itself when we create a slice of state to hold the value of the theme.
export const ThemeProvider = ({ initialTheme, children }) => {
const [theme, setTheme] = React.useState(getInitialTheme);
...
}
Next, we create a function called rawSetTheme
which takes in a theme value as a parameter.
This function grabs the element that is the root element of the document in our React application. It stores this element in a variable named root
.
const root = window.document.documentElement;
Next, we create a boolean variable named isDark
. This variable needs to determine if the theme that was passed to this function was set to dark
. Writing an if
statement here is overkill, so instead we can do it all on one line.
const isDark = rawTheme === 'dark';
Now that we have these pieces, we can dynamically remove the old theme class value on the root element, and then replace it with the new theme value! This is important! Remember, Tailwind looks for an element in the DOM that has a dark
class applied to it. If that element is found, all of the dark
variant styles will be applied. We've just completed this functionality with a few lines of code. Cool, huh?
root.classList.remove(isDark ? 'light' : 'dark');
root.classList.add(rawTheme);
Finally, we store the user's theme value in local storage so that it is saved for the next time the user visits the web app (the getInitialTheme()
function above looks for this value and returns the appropriate value).
localStorage.setItem('color-theme', rawTheme);
Next up, outside of the rawSetTheme()
function, we create an if
statement which checks to see if an initial theme value was passed to the component. If so, we send this value straight to the rawSetTheme()
function and it takes care of the rest.
if (initialTheme) {
rawSetTheme(initialTheme);
}
What comes next is super cool. We incorporate a useEffect
hook in the component which calls the rawSetTheme()
function whenever the theme
variable is changed. The real power of this comes into play later when other components want to change the value of the theme.
React.useEffect(() => {
rawSetTheme(theme);
}, [theme]);
Finally, we have the mandatory return
statement which renders the elements passed as children to this component and wraps them in ThemeContext.Provider
so all of the children have access to theme
and setTheme
.
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
Again, now that other components have access to the theme
variable and when they change the value, the rawSetTheme()
function inside of this component gets triggered. Are you starting to see the power of this? Good!
Creating the Toggle
Next, create a new file called themeToggle.js
and set up the basic component structure.
import React from 'react';
const Toggle = () => {
return (
<div>
...
</div>
)
}
export default Toggle;
Because we've exposed some state with the Context API, we can access the state in this component.
First, import ThemeContext
(which is what we exported in the previous file), pass it to the useContext
hook and grab theme
and setTheme
from the context.
import { ThemeContext } from './themeContext';
const Toggle = () => {
const { theme, setTheme } = React.useContext(ThemeContext);
...
}
Now we can render icons dynamically depending on the theme value. When an icon is clicked, call the setTheme()
function and pass the appropriate value.
NOTE: I've installed react-icons and am using the sun and moon icons from Hero Icons
Here is the full Toggle
component.
import React from 'react';
import { HiMoon, HiSun } from 'react-icons/hi';
import { ThemeContext } from './themeContext';
const Toggle = () => {
const { theme, setTheme } = React.useContext(ThemeContext);
return (
<div className="transition duration-500 ease-in-out rounded-full p-2">
{theme === 'dark' ? (
<HiSun
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="text-gray-500 dark:text-gray-400 text-2xl cursor-pointer"
/>
) : (
<HiMoon
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="text-gray-500 dark:text-gray-400 text-2xl cursor-pointer"
/>
)}
</div>
);
};
export default Toggle;
Putting it all Together
Now that we have the themeProvider
and Toggle
components, let's use them in the index.js
file.
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<main>
<div className="absolute right-0 top-0 mr-4 mt-4 md:mr-6 md:mt-6">
<Toggle />
</div>
<App />
</main>
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
);
We want the toggle component to be positioned in the top-right corner of the browser, so we use some absolute position Tailwind utilities to get this working.
The text color should change at this point, but what's the point if the background doesn't change? Let's go ahead and generate a new component that controls the color of the background.
Generate a new file called background.js
and paste the following code:
const Background = ({children}) => {
return (
// Remove transition-all to disable the background color transition.
<body className="bg-white dark:bg-black transition-all">
{children}
</body>
)
}
export default Background;
Head back to the index.js
file and wrap the <main>
tag inside the new Background
component.
ReactDOM.render(
<React.StrictMode>
<ThemeProvider>
<Background>
<main>
<div className="absolute right-0 top-0 mr-4 mt-4 md:mr-6 md:mt-6">
<Toggle />
</div>
<App />
</main>
</Background>
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
);
Now you can run the app using npm start
and see the background change from black to white when the toggle is clicked! Nice!
Conclusion
Tailwind v2.0 gives us the flexibility to choose how we want to implement dark mode. In this example, we utilized the Context API to expose a theme variable and a function to change the value. When the value is changed, it triggers a function in the ThemeProvider
component that adds a class to the root element. If Tailwind sees a dark
class in the DOM, the dark
variant styles are applied to your elements. Super cool!
We've covered a lot of ground in this article and if you made it all of the way through, make sure to let me know in the comments down below. You're the real MVP!
Before wrapping up, I want to let you know about a gift I made for you!
I've created a React starter template - Tailwind v2.0 is set up and ready-to-go with dark mode support!
You can clone the repo HERE!
Here's a preview of what you get with the template:
If you liked this article and want more content like this, read some of my other articles , subscribe to my newsletter and make sure to follow me on Twitter!
Top comments (4)
uuh, I love dark mode
nice tutorial
Glad you liked the tutorial!
Great stuff.
Great tutorial with a step by step breakdown. The internet needs more content like this.