โ€ข 11 min read โ€ข Last updated:

How to Dark Mode in React and Tailwind CSS

Dark mode is the first feature I added in my website. I really didn't know how to do it at first, especially I'm using Tailwind for my styling. I'm sure there are plugins available to use but I want to implement it myself in order to learn more about React and CSS.

Good thing I came across this beautiful article by Josh Comeau: The Quest for the Perfect Dark Mode. Two things became clear to me: CSS variables and prefers-color-scheme media query.

In this post, I will walk you through my process on how to implement dark mode in a Gatsby and Tailwind CSS project.

๐Ÿ’ก This tutorial assumes that you have a basic understanding of CSS variables and React's Context API.

This project uses Tailwind, follow their documentation on how to install and setup Tailwind.

๐Ÿš€ You can find the source in GitHub and the demo here.

Adding our CSS variables

First, let's declare all our css variables in our main css file. If you don't know which file it is, it's where you put the tailwind imports.

In my website I tried to stick with five colors: primary, secondary, and accent, for both background and texts. This will differ based on your design, but in my case, I already knew what colors I needed because I designed my website in Figma beforehand.

Color Palette

Next, add .light and .dark CSS classes and assign the colors for each variables. Then use the @apply directive in the root selector to apply a default theme for your page.

index.css
:root {
@apply .light;
}
.dark {
--color-bg-primary: #2d3748;
--color-bg-secondary: #283141;
--color-text-primary: #f7fafc;
--color-text-secondary: #e2e8f0;
--color-text-accent: #81e6d9;
}
.light {
--color-bg-primary: #ffffff;
--color-bg-secondary: #edf2f7;
--color-text-primary: #2d3748;
--color-text-secondary: #4a5568;
--color-text-accent: #2b6cb0;
}
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

Extending Tailwind CSS

In order to use the css variables we created, we must extend the tailwind configuration.

tailwind.config.js
module.exports = {
theme: {
extend: {
backgroundColor: {
primary: 'var(--color-bg-primary)',
secondary: 'var(--color-bg-secondary)',
},
textColor: {
accent: 'var(--color-text-accent)',
primary: 'var(--color-text-primary)',
secondary: 'var(--color-text-secondary)',
},
},
},
}

These extensions will now be included in Tailwind classes

Tailwind Intellisense

๐Ÿš€ Install this VS Code Extension for that sweet, sweet, Tailwind intellisense.

Adding a toggle

Before we create a way for the user to toggle the theme between light or dark theme, we must first prepare our React context.

Getting the initial theme

themeContext.jsx
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 light theme as the default,
// return "light" instead
return 'dark'
}

We are doing multiple things here: first we check if we already have a stored value in the localStorage. If not, we check the media query if the user browser prefers a dark or light color scheme using prefers-color-scheme media query.

Creating our context

If you have no idea what a context is in React, please read their documentation. We are using the Context API to pass our theme data without having to pass the prop down manually in every component.

Our theme context must do the following:

  1. Create a state for the theme and pass the getInitialTheme function that we wrote earlier to get the initial state value.
  2. Create another function called rawSetTheme that will apply the .light or .dark class in the root element and save the theme in the localStorage
  3. Create a side effect that will call the rawSetTheme whenever the value of theme changes.
themeContext.jsx
export const ThemeContext = createContext()
export const ThemeProvider = ({ initialTheme, children }) => {
const [theme, setTheme] = useState(getInitialTheme)
const rawSetTheme = theme => {
const root = window.document.documentElement
const isDark = theme === 'dark'
root.classList.remove(isDark ? 'light' : 'dark')
root.classList.add(theme)
localStorage.setItem('color-theme', theme)
}
if (initialTheme) {
rawSetTheme(initialTheme)
}
React.useEffect(
_ => {
rawSetTheme(theme)
},
[theme]
)
return (
<ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider>
)
}

Using the context provider

For our components to use the context, let's make the ThemeProvider as the Higher Order Component (HOC).

layout.jsx
import { ThemeProvider } from './themeContext'
const Layout = ({ children }) => {
return (
<ThemeProvider> <Header /> <main>{children}</main> </ThemeProvider>
)
}

Adding the toggle functionality

Now that we have our context ready, let's create a toggle component that will let the user switch the theme.

  1. Use the ThemeContext to get the theme and setTheme.
  2. Set the checkbox's checked attribute to true when the theme is equal to dark
  3. Call the setTheme on the onChange event.
toggle.jsx
export const Toggle = () => {
const { theme, setTheme } = useContext(ThemeContext)
function isDark() {
return theme === 'dark'
}
function toggleTheme(e) {
setTheme(e.target.checked ? 'dark' : 'light')
}
return (
<label> <input type="checkbox" checked={isDark()} onChange={e => toggleTheme(e)} /> Dark Mode </label>
)
}

Yay!

And there you have it! Our toggle is not as fancy as it looks, but you can do the same logic using a button or a different component.

Final dark mode output

This solution isn't perfect and there's a small caveat with this: page load flicker. Josh made a great write up about how he solved it in the same article.

Dark mode design

I took inspirations from many other websites for the dark mode design of this website. Then, I used TailwindCSS plugin for Figma to simplify my color selection.

However, if you're interested on designing your own dark UI, check out this in-depth article about Dark UI by Miklos Philips.

June 2021 Update

After reading Use CSS Variables instead of React Context by Kent C. Dodds, I realized that since we're already using CSS variables, we don't necessarily need to use a Context in our app.

Using a Context rerenders the components inside the Provider and might cause performance issues. Instead, we just need the toggle logic by creating a custom hook useDarkMode:

userDarkMode.js
export function useDarkMode() {
const prefersDarkMode = usePrefersDarkMode()
const [
isEnabled,
setIsEnabled] = useSafeLocalStorage('dark-mode', undefined)
const enabled =
isEnabled === undefined ? prefersDarkMode : isEnabled
useEffect(() => {
if (window === undefined) return
const root = window.document.documentElement
root.classList.remove(enabled ? 'light' : 'dark')
root.classList.add(enabled ? 'dark' : 'light')
}, [enabled])
return [enabled, setIsEnabled]
}

This is how the toggle component looks like now:

toggle.jsx
export const Toggle = () => {
const [isDark, setIsDark] = useDarkMode()
return (
<label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Dark Mode </label>
)
}

Now, with the ThemeContext we created earlier gone, our code is much more cleaner and performant.

A small favor

Is this post confusing? Did I make a mistake? Let me know if you have any feedback and suggestions!