It’s become popular for applications to have a setting that lets you toggle between dark and light modes. Maybe it’s due to the popularity of dark UIs, maybe it’s because apps are gradually becoming more configurable.

React context is an easy way of sharing data globally, but it can make component reuse more difficult. As an alternative, you can build a dark mode button component that uses the useEffect and useState hooks instead of context. It will toggle a data attribute on the body element that CSS styles can target.

What You’ll Need

To follow along with this tutorial, you’ll need the following:

  • A recent version of Node installed on your machine.
  • A basic understanding of React and React hooks.
  • A starter React project. Just create a React app and you’re ready to go.

Create a Button Component

The button component will be responsible for toggling the theme from dark to light. In a real application, this button might be part of the Navbar component.

In the src folder, create a new file called Button.js and add the following code.

        import { useState } from 'react'
 
export default function Button() {
    const [theme, settheme] = useState("dark")
 
    const handleToggle = () => {
const newTheme = theme === "light" ? "dark" : "light"
settheme(newTheme)
    }

    return (
        <>
            <button className="themeBtn" onClick={handleToggle}>
                {theme=== "light" ? <span>dark</span> : <span>light</span>}
            </button>
        </>
    )
}

First, import the useState() hook from React. You will use it to keep track of the current theme.

In the Button component, initialize the state to dark. The handleToggle() function will take care of the toggling functionality. It runs each time the button is clicked.

This component also toggles the button text when it changes the theme.

To display the Button component import it into App.js.

        import Button from './Button';

function App() {
    return (
        <div>
            <Button/>
        </div>
    );
}
 
export default App;

Create the CSS Styles

Right now, clicking the button does not change the UI of the React app. For that, you will first need to create the CSS styles for dark and light mode.

In App.css, add the following.

        body {
  --color-text-primary: #131616;
  --color-text-secondary: #ff6b00;
  --color-bg-primary: #E6EDEE;
  --color-bg-secondary: #7d86881c;
  background: var(--color-bg-primary);
  color: var(--color-text-primary);
  transition: background 0.25s ease-in-out;
}

body[data-theme="light"] {
  --color-text-primary: #131616;
  --color-bg-primary: #E6EDEE;
}

body[data-theme="dark"] {
  --color-text-primary: #F2F5F7;
  --color-bg-primary: #0E141B;
}

Here, you are defining the styles of the body element using data attributes. There is the light theme data attribute and the dark theme data attribute. Each of them has CSS variables with different colors. Using CSS data attributes will allow you to switch the styles according to the data. If a user selects a dark theme, you can set the body data attribute to dark and the UI will change.

You can also modify the button element styles to change with the theme.

        .themeBtn {
  padding: 10px;
  color: var(--color-text-primary);
  background: transparent;
  border: 1px solid var(--color-text-primary);
  cursor: pointer;
}

Modify Button Component to Toggle Styles

To toggle the styles defined in the CSS file, you will need to set the data in the body element in the handleToggle() function.

In Button.js, modify handleToggle() like this:

        const handleToggle = () => {
    const newTheme = theme ==="light" ? "dark" : "light"
    settheme(newTheme)
    document.body.dataset.theme = theme
}

If you click on the button, the background should toggle from dark to light or light to dark. However, if you refresh the page, the theme resets. To persist the theme setting, store the theme preference in local storage.

Persisting User Preference in Local Storage

You should retrieve the user preference as soon as the Button component renders. The useEffect() hook is perfect for this as it runs after every render.

Before retrieving the theme from the local storage, you need to store it first.

Create a new function called storeUserPreference() in Button.js.

        const storeUserSetPreference = (pref) => {
    localStorage.setItem("theme", pref);
};

This function receives the user preference as an argument and stores it as an item called theme.

You will call this function every time the user toggles the theme. So, modify the handleToggle() function to look like this:

        const handleToggle = () => {
    const newTheme = theme === "light" ? "dark" : "light"
    settheme(newTheme)
    storeUserSetPreference(newTheme)
    document.body.dataset.theme = theme
}

The following function retrieves the theme from local storage:

        const getUserSetPreference = () => {
    return localStorage.getItem("theme");
};

You will use it in the useEffect hook so each time the component renders, it fetches the preference from local storage to update the theme.

        useEffect(() => {
    const userSetPreference = getUserSetPreference();
 
    if (userSetPreference) {
        settheme(userSetPreference)
    }

    document.body.dataset.theme = theme
}, [theme])

Getting User Preference From Browser Settings

For an even better user experience, you can use the prefers-color-scheme CSS media feature to set the theme. This should reflect a user’s system settings that they can control via their OS or browser. The setting can either be light or dark. In your application, you would need to check this setting immediately after the button component loads. This means implementing this functionality in the useEffect() hook.

First, create a function that retrieves the user preference.

In Button.js, add the following.

        const getMediaQueryPreference = () => {
    const mediaQuery = "(prefers-color-scheme: dark)";
    const mql = window.matchMedia(mediaQuery);
    const hasPreference = typeof mql.matches === "boolean";
    
    if (hasPreference) {
        return mql.matches ? "dark" : "light";
    }
};

Next, modify the useEffect() hook to retrieve the media query preference and use it if no theme is set in the local storage.

        useEffect(() => {
    const userSetPreference = getUserSetPreference();
    const mediaQueryPreference = getMediaQueryPreference();
 
    if (userSetPreference) {
        settheme(userSetPreference)
    } else {
        settheme(mediaQueryPreference)
    }
 
    document.body.dataset.theme = theme
}, [theme])

If you restart your application, the theme should match your system’s settings.

Using React Context to Toggle Dark Mode

You can use data attributes, CSS, and React hooks to toggle the theme of a React application.

Another approach to handling dark mode in React is to use the context API. React context allows you to share data across components without having to pass it down through props. When using it to toggle themes, you create a theme context that you can access throughout the application. You can then use the theme value to apply matching styles.

While this approach works, using CSS data attributes is simpler.