How to implement Dark Mode in Tailwind CSS

Tailwind Dark Mode

Dark mode this design trend has almost become expected by most of the users on the internet 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 files got so weird and it’s hard to read. With Tailwind CSS, Dark more is build right into the framework and removes the need to work with those CSS variables!

In this article, i’ll show you how to get dark mode working in a react application. Even though this is done with react, the main ideas apply to all modern JavaScript frameworks.

Here’s an example of styling a component with dark mode :

1<div class="bg-white dark:bg-gray-800">2  <h1 class="text-gray-900 dark:text-white">Dark mode is here!</h1>3  <p class="text-gray-600 dark:text-gray-300">Lorem ipsum...</p>4</div>

It’s up to us to supply the functionality that dynamically adds/removes the 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 that adds/removes the class on the root document element.

I prefer to break features down into smaller chunks before we begin to work on them. It’ll help us understand requirements, identify and better think about the problem.

The first thing we should figure our is how Tailwind wants developers to work with the new dark mode features.

According to the official documentation of tailwind css, 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 their style utilities. You can mix 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">2  <h1 class="text-gray-900 dark:text-white">Dark mode is here!</h1>3  <p class="text-gray-600 dark:text-gray-300">Lorem ipsum...</p>4</div>

Styling elements with the variant won't actually do anything until dark mode is enabled in the project.

In the file, we can include the option to enable the feature in Tailwind.

It’s important to note that there are two values we can use here — and .

The value uses the 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 , styles your elements with the variant, and you're good to go!

1// tailwind.config.js2module.exports = {3  darkMode: "media",4  // ...5};

On the other hand, if we want to include a manual appearance switch on the web application, we can use the 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 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.

1// tailwind.config.js2module.exports = {3  darkMode: "class",4  // ...5};

Continuing the thought above, in order for the dark mode styles to take effect, Tailwind searches through the DOM for an element with a class applied to it. If it finds that element, all of the 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, but the next step is to actually enable dark mode in our file. Make sure you set to a value of .

// tailwind.config.js2module.exports = {3  darkMode: "class",4  // ...5};

Removing Boilerplate Code

Open the file and replace with boilerplate code with the following markup.

1import "./App.css";23function App() {4  return (5    <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">6      <div className="min-h-screen flex justify-center items-center">7        <h1 className="text-gray-900 dark:text-white text-3xl sm:text-5xl lg:text-6xl leading-none font-extrabold tracking-tight mb-8">8          Dark Mode Template9        </h1>10      </div>11    </div>12  );13}1415export default App;

We set some max-width breakpoints on the outer and incorporate the variant on the 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 ) called and paste the following code.

1import React from "react";23const getInitialTheme = () => {4  if (typeof window !== "undefined" && window.localStorage) {5    const storedPrefs = window.localStorage.getItem("color-theme");6    if (typeof storedPrefs === "string") {7      return storedPrefs;8    }910    const userMedia = window.matchMedia("(prefers-color-scheme: dark)");11    if (userMedia.matches) {12      return "dark";13    }14  }1516  // If you want to use dark theme as the default, return 'dark' instead17  return "light";18};1920export const ThemeContext = React.createContext();2122export const ThemeProvider = ({ initialTheme, children }) => {23  const [theme, setTheme] = React.useState(getInitialTheme);2425  const rawSetTheme = (rawTheme) => {26    const root = window.document.documentElement;27    const isDark = rawTheme === "dark";2829    root.classList.remove(isDark ? "light" : "dark");30    root.classList.add(rawTheme);3132    localStorage.setItem("color-theme", rawTheme);33  };3435  if (initialTheme) {36    rawSetTheme(initialTheme);37  }3839  React.useEffect(() => {40    rawSetTheme(theme);41  }, [theme]);4243  return (44    <ThemeContext.Provider value={{ theme, setTheme }}>45      {children}46    </ThemeContext.Provider>47  );48};

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 component in a few minutes.

1export const ThemeContext = React.createContext();

The 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 as the value. If the user has not selected as their default system appearance, this function returns . Note that if you want your application to show a dark appearance by default, you can alter this function to return .

This function is being called inside the component itself when we create a slice of state to hold the value of the theme.

1export const ThemeProvider = ({ initialTheme, children }) => {2    const [theme, setTheme] = React.useState(getInitialTheme);3...4}

Next, we create a function called 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 .

1const root = window.document.documentElement;

Next, we create a boolean variable named . This variable needs to determine if the theme that was passed to this function was set to . Writing an statement here is overkill, so instead we can do it all on one line.

1const 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 class applied to it. If that element is found, all of the variant styles will be applied. We've just completed this functionality with a few lines of code. Cool, huh?

1root.classList.remove(isDark ? "light" : "dark");2root.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 function above looks for this value and returns the appropriate value).

1localStorage.setItem("color-theme", rawTheme);

Next up, outside of the function, we create an statement which checks to see if an initial theme value was passed to the component. If so, we send this value straight to the function and it takes care of the rest.

1if (initialTheme) {2  rawSetTheme(initialTheme);3}

What comes next is super cool. We incorporate a hook in the component which calls the function whenever the variable is changed. The real power of this comes into play later when other components want to change the value of the theme.

1React.useEffect(() => {2  rawSetTheme(theme);3}, [theme]);

Finally, we have the mandatory statement which renders the elements passed as children to this component and wraps them in so all of the children have access to and .

1return (2  <ThemeContext.Provider value={{ theme, setTheme }}>3    {children}4  </ThemeContext.Provider>5);

Again, now that other components have access to the variable and when they change the value, the function inside of this component gets triggered. Let’s see the next thing? Nice!

Creating the Toggle

Next, we need to create a new file called and set up the basic component structure.

import React from "react";23const Toggle = () => {4  return <div>...</div>;5};67 export default Toggle;

Because we’ve exposed some state with the Context API, we can access the state in this component.

First, import (which is what we exported in the previous file), pass it to the hook and grab and from the context.

1import { ThemeContext } from './themeContext';23const Toggle = () => {4    const { theme, setTheme } = React.useContext(ThemeContext);5...6}

Now we can render icons dynamically depending on the theme value. When an icon is clicked, call the function and pass the appropriate value.

I’ve installed react-icons and am using the sun and moon icons from Hero Icons for the toggle.

Here's the full component.

import React from "react";2import { HiMoon, HiSun } from "react-icons/hi";3import { ThemeContext } from "./themeContext";45const Toggle = () => {6  const { theme, setTheme } = React.useContext(ThemeContext);78  return (9    <div className="transition duration-500 ease-in-out rounded-full p-2">10      {theme === "dark" ? (11        <HiSun12          onClick={() => setTheme(theme === "dark" ? "light" : "dark")}13          className="text-gray-500 dark:text-gray-400 text-2xl cursor-pointer"14        />15      ) : (16        <HiMoon17          onClick={() => setTheme(theme === "dark" ? "light" : "dark")}18          className="text-gray-500 dark:text-gray-400 text-2xl cursor-pointer"19        />20      )}21    </div>22  );23};2425export default Toggle;

Putting it all Together

Now that we have the and components, let's use them in the file.

1ReactDOM.render(2  <React.StrictMode>3    <ThemeProvider>4      <main>5        <div className="absolute right-0 top-0 mr-4 mt-4 md:mr-6 md:mt-6">6          <Toggle />7        </div>8        <App />9      </main>10    </ThemeProvider>11  </React.StrictMode>,12  document.getElementById("root")13);

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 and paste the following code:

1const Background = ({ children }) => {2  return (3    // Remove transition-all to disable the background color transition.4    <body className="bg-white dark:bg-black transition-all">{children}</body>5  );6};78export default Background;

Head back to the file and wrap the tag inside the new component.

1ReactDOM.render(2  <React.StrictMode>3    <ThemeProvider>4      <Background>5        <main>6          <div className="absolute right-0 top-0 mr-4 mt-4 md:mr-6 md:mt-6">7            <Toggle />8          </div>9          <App />10        </main>11      </Background>12    </ThemeProvider>13  </React.StrictMode>,14  document.getElementById("root")15);

Now you can run the app using and see the background change from black to white when the toggle is clicked!Awesome! You’re good to go!

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 component that adds a class to the root element. If Tailwind sees a class in the DOM, the variant style is applied to your element, That’s Super Cool!

Here’s a preview of what you get with the Tailwind CSS Dark mode toggle:

Thanks for reading! If you liked my content then Share this article with anybody you think would benefit from this. If you have any suggestions, feel free to hit me up. I hope you enjoyed this article. If you did, make sure to let me know in the comments down below and don’t hesitate to buy me a coffee by clicking below👇!

Thank You!

Darshan Mandade
(PAPA Team Writer)

Python Fullstack Developer | Blogger | Web Developer | Mentor. Let's Connect on IG: @Darshu.codes