React dark mode toggle with tailwind

I have seen countless number of times the question about how to toggle dark mode in React or NextJS when using Tailwind so, instead of answering the question every time, I though that it would be better to have a reference here to point to. So here we go!

 

What are React, NextJS and tailwind?

To get a bit of context for those who don’t know one of those:

  • React is a javascript frontend library that allow developers to create reusable components. This mean that you can define a unit that you can reuse in the rest of your code. For example, you can create a button with some specific action and design that you call MyFancyButton and can use this name as any other HTML tag in other part of your application.

  • NextJS is a framework based on top of React for building full stack web application. Meaning that, you basically use React for the interface and NextJS provides extra functionality and tooling for, amongst other things, bundling, compiling, routing, server side rendering, optimizations, and more…

  • Tailwind is a CSS framework that contains hundreds of predefined classes that you can mix and match to create beautiful UI and will analyse your code to ship only the CSS you use.

 

How does dark mode work with tailwind?

Tailwind will allow developer to add classes with the prefix dark: which will allow different style in dark mode. For instance

<div class="bg-white dark:bg-black">
  <span class="dark:text-white">Hello World!</span>
</div>

will create a div that will change background color and the span will change text color when switching between light and dark mode.

By default, Tailwind will use the prefers-color-scheme CSS media query to verify what the settings should be.

 

Nice! But I want to set the mode manually. How?

As the article title would suggest, we want to be able to propose to the user to change the code. This mean being able to switch between dark mode without changing the computer settings. Luckily, Tailwind provides an alternative way to check for dark mode (than CSS media query). You can tell Tailwind to use a class name instead by changing the configuration in tailwind.config.js and add

/** @type {import('tailwindcss').Config} */
module.exports = {
    // ...
    darkMode: 'class',
    // ...
}

Now Tailwind will assume that every DOM children of a tag with a class name dark is in dark mode. So all you have to program, is a way to remove or add this class.

 

Just add a class name?

Yes. That is all there is to it to toggle between light and dark mode. If you just want your users to choose between light and dark, you can add a radio button group or a drop down to switch between the 2 and save a settings to local storage or a cookie and read from it when loading a page. A simple example could look like:

// On page load or when changing themes
if (localStorage.theme === 'dark') {
  document.documentElement.classList.add('dark')
} else {
  document.documentElement.classList.remove('dark')
}

// Whenever the user explicitly chooses light mode
localStorage.theme = 'light'

// Whenever the user explicitly chooses dark mode
localStorage.theme = 'dark'

But you might also want to take the system settings into account. And, if so, you probably might want to change the class when time goes by and the system switches to dark mode at night.

I want it all, can you give me a code example?

Sure. Here is what I think we need:

  • a function to update mode when the dropdown changes. We will use local storage to save the user’s choice

  • a function to add a listener for when the system setting changes

Both functions must take into account changes in both the system setting and local storage. Here is how I think it could look like:

// Update based on changes from the user
    const updateMode = () => {
      // check if the setting already exists in localstorage or not
        if (!('theme' in localStorage)) {
          // if not, check if we can get it from the media query
            const mql = window.matchMedia('(prefers-color-scheme: dark)')
            if (mql.matches) {
                document.documentElement.classList.add('dark')
            } else {
                document.documentElement.classList.remove('dark')
            }
        } else {
          // if the setting exist in local storage, use it
            if (localStorage.theme === 'dark') {
                document.documentElement.classList.add('dark')
            } else {
                document.documentElement.classList.remove('dark')
            }
        }
    }

// Add listener
    const addChangeModeListener = () => {
        const mql = window.matchMedia('(prefers-color-scheme: dark)')
        mql.addEventListener('change', () => {
          // dark mode if local storage has the setting or not in local storage and media query matches
            if (localStorage.theme === 'dark' || (!('theme' in localStorage) && mql.matches)) {
                document.documentElement.classList.add('dark')
            } else {
                document.documentElement.classList.remove('dark')
            }
        })
    }

// In a React Component, you might want to call those 2 like so
    useEffect(() => {
        addChangeModeListener()
        updateMode()
    }, [])


// Lastly, you need to call the changeMode when you want to change
// Change the local storage
    const changeMode = (value: string) => {
        if (value === 'system') {
            localStorage.removeItem('theme')
        } else {
            localStorage.theme = value
        }
        updateMode()
    }
// buttons to call the function
return (
<>
  <button onClick={() => changeMode('light')}>Light</button>
  <button onClick={() => changeMode('dark')}>Dark</button>
  <button onClick={() => changeMode('system')}>System</button>
</>
)

Here I show it with 3 buttons, but it can be radio, dropdown change, etc…

 

I just want a component I can use

If you really want to get something going without any more work, you can have a look at https://github.com/Agilxp/DarkModeToggle which is where the code shown here resides. You can use it as a component directly as well with yarn add @agilxp/dark-mode-toggle or npm. Installation instruction on the page.

In addition to the code in this post, it will cope with some Server Side rendering issue like missing window and will retry.

Work is still in progress and improvements will come but, for now, it uses Tailwind for some design and you can only customize the icon that is shown. Default is a sun for light mode and a moon for dark mode (and a computer for system).

You can see it at work at https://agilxp.github.io/DarkModeToggle. Let me know if you have issues with it or just if you like it.

Happy toggling!

Forrige
Forrige

Running Puppeteer on Vercel

Neste
Neste

NextJS 14: A Closer Look at the Latest Enhancements and Announcements