Add Dark Mode to React

April 09, 2024
article featured image
A lot of developers like to view their code using a dark color scheme, and often that applies to websites as well. We also know that for those sensitive to bright color palletes, it's nice to have options. This article will detail my efforts in apply dark mode to Gatsby projects.

Table of Contents

    §  Create Dark/Light properties on Body Tag

    There are Gatsby plugin options to incorporate DarkMode, but in this case we will use React's Context capability. This will let any React component read properties from React Context.

    At the very root of your SCSS file, let's incorporate some CSS variables that we can use throughout the application. At the root level, we want to esablish the colors of these variables will change if the root class of your body element has either the dark class or not.

    :root {
      --primary-contrast-color: #FFFFFF;
      --secondary-contrast-color: #000000;
    
      [...]
    
      --primary-background: #000000;
      --secondary-background: #FFFFFF;
    
    }
    
    body.dark {
      --primary-contrast-color: #000000;
      --secondary-contrast-color: #FFFFFF;
    
      [...]
    
      --primary-background: #FFFFFF;
      --secondary-background: #000000;
    
    }
    

    Now that we've done this, we can now use this variable to change colors of icons or other elements if this root class of dark is present or not.

    .genButton {
      text-align: center;
      transition: ease 0.3s;
      border: 4px solid var(--secondary-contrast-color);
      background-color: var(--secondary-background);
      padding: 0.5em 1.1em;
      top: 0;
      box-shadow: none;
      box-sizing: border-box;
      text-decoration: none;
      color: var(--primary-contrast-color);
    }

    §  Create Context Component

    Before we create the Toggle Button, we will create a component that holds our variable. This variable will also use localStorage on the user's browser to hold the boolean that stores whether dark mode is on or off.

    (You can read more about using React's Context API with Gatsby here )

    import { createContext, useContext, useEffect, useState } from 'react';
    
    interface AdvancedThemeContextProps {
      isDarkMode: boolean;
      toggleDarkMode: () => void;
    }
    
    export const AdvancedThemeContext = createContext<
      AdvancedThemeContextProps | undefined
    >(undefined);
    
    const AdvancedThemeProvider = ({ children }) => {
        // use react hooks for initial variable
      const [isDarkMode, setIsDarkMode] = useState(false); 
    
      const toggleDarkMode = () => {
        const newDarkModeState = !isDarkMode;
        setIsDarkMode(newDarkModeState);
        // use localStorage on user's browser
        localStorage.setItem('isDarkMode', JSON.stringify(newDarkModeState));
      };
    
      useEffect(() => {
        const storedDarkMode = localStorage.getItem('isDarkMode');
        if (storedDarkMode) {
          setIsDarkMode(JSON.parse(storedDarkMode));
        }
      }, []);
    
      useEffect(() => {
        document.body.classList.toggle('dark', isDarkMode);
      }, [isDarkMode]);
    
      return (
        <AdvancedThemeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
          {children}
        </AdvancedThemeContext.Provider>
      );
    };
    
    const useTheme = () => {
      const context = useContext(AdvancedThemeContext);
      if (!context) {
        throw new Error('useTheme must be used within a ThemeProvider');
      }
      return context;
    };
    
    export { AdvancedThemeProvider, useTheme };
    

    This code does two things. It changes the root class of body to dark on and off, and also it allows you to use the isDarkMode variable in any other part of your application you choose to wrap with AdvancedThemeProvider. This can be useful if you are using Bootstrap Components that change their dark-mode properties on something other than a CSS class, such as data-bs-theme.

    import { Form } from 'react-bootstrap';
    import { AdvancedThemeContext } from 'contexts/AdvancedThemeContext';
    
    const FormThemeWrapper = ({ children, className, onSubmit }) => {
      return (
        <AdvancedThemeContext.Consumer>
          {({ isDarkMode }) => {        return (
              <Form
                className={className}
                onSubmit={onSubmit}
                data-bs-theme={isDarkMode ? 'dark' : 'light'}          >
                {children}
              </Form>
            );
          }}
        </AdvancedThemeContext.Consumer>
      );
    };
    
    export default FormThemeWrapper;

    §  DarkMode Toggle Button

    I then create a DarkMode component I will later insert into my Top Banner.

    import React from 'react';
    import DarkModeIcon from 'assets/official-dark.svg';
    import { useTheme } from 'contexts/AdvancedThemeContext';
    
    const DarkStateToggle: React.FC = () => {
      const { toggleDarkMode } = useTheme();
      return (
        <div
          onClick={() => {
            toggleDarkMode();
          }}
          style={{ padding: '10px 20px', cursor: 'pointer' }}
        >
          <DarkModeIcon
            className="regularIcon"
            role="button"
            onClick={() => {
              toggleDarkMode();
            }}
          />
        </div>
      );
    };

    §  DarkMode Default

    The last point, if you'd like to default the app for dark mode, the easiest way to add it is to create a gatsby-ssr.tsx file and add it here.

    exports.onRenderBody = ({ setBodyAttributes, setHtmlAttributes }) => {
      setHtmlAttributes({ lang: 'en' });
       setBodyAttributes({
         className: 'dark',   });
    };
    

    Good luck!