DEV Community

Cover image for Light/Dark mode toggle with React using Context API and styled components
Sorin Chis
Sorin Chis

Posted on

Light/Dark mode toggle with React using Context API and styled components

Have you ever wonder how to build a dark/light theme with React? after checking a couple of solution I've decided to build a basic and simple theme switcher from scratch using Context API and styled-components.

This is just one way of doing it from many ..many more 🤘💥. If you are curious about what we are gonna build here you can see the live demo of the final version and full code from github here.

If you don't already have an ReactJS application you can easily create one with create-react-app.

Once you have your react app running you will have to install 3 packages:

  • styled-components are one of the new ways to use CSS in modern JavaScript. It is the meant to be a successor of CSS Modules, a way to write CSS that's scoped to a single component, and not leak to any other element in the page.
  • react-switch we will use this library for switch button
  • react-icons popular icons in your React projects easily, we will use it for bringing light and dark icons.

First we will create a component AppProvider.js in which we will define the initial state of our theme mode using useState and then create an application context with createContext function.

Context provides a way to pass data through the component tree without having to pass props down manually at every level. The theme mode will be available through useContext hook.

Every Context object comes with a Provider React component that accepts a value prop. This value will be passed to down to all Provider descendants.

import React, { createContext, useState } from "react";

export const AppContext = createContext();

const AppProvider = ({ children }) => {
  const [themeMode, setThemeMode] = useState("lightTheme");

  const value = { themeMode };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};

export default AppProvider;

Once we have the AppProvider we can simply import it in index.js file and wrap all our application with it

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import AppProvider from "./AppProvider";

ReactDOM.render(
  <AppProvider>
    <App />
  </AppProvider>,
  document.getElementById("root")

Now it will be a perfect time to check if our Provider it's doing its job by bringing the initial theme mode in one of the Provider descendants. Let's create a new component ThemeSwitcher.js and import it in App.js

import React from "react";
import ThemeSwitcher from "./ThemeSwitcher";
function App() {
  return (
    <>
      <ThemeSwitcher />
    </>
  );
}

export default App;

Using useContext hook we we will gain access to our initial theme mode lightTheme. Now you can really see the power of context API - we can pass the state and manage it without importing any library 👏💪

import React, { useContext } from "react";
import { AppContext } from "./AppProvider";

const ThemeSwitcher = () => {
  const { themeMode } = useContext(AppContext);
  console.log("THEME MODE: ", themeMode);
  return <div></div>;
};

export default ThemeSwitcher;

Once we check that everything it's working we will start creating the theme colors and global styles for our beautiful application. Let's add a folder called styles in our src folder and create a theme.js file.

theme.js - in which will keep our theme colors

export default {
  lightTheme: {
    colors: {
      background: 'radial-gradient(lightGrey, black)',
      black: 'black',
      white: 'white',
      blue: '#a0e9fd',
      lightBlue: '#caf3fe',
      secondary: 'radial-gradient(green, yellow)',
    },
    transition: '0.3s',
  },
  darkTheme: {
    colors: {
      background: 'radial-gradient(black, lightGrey)',
      black: 'white',
      white: 'black',
      blue: '#a0e9fd',
      lightBlue: '#caf3fe',
      secondary: 'radial-gradient(yellow, green)',
    },
    transition: '0.3s',
  },
}

After creating the theme we will need to import this file in our provider and bring the ThemeProvider from styled-components

import { ThemeProvider } from 'styled-components'
import { theme } from './styles'

Now we are ready to wrap our application with the ThemeProvider. Create a costumTheme constant and with the help of our useState hook (which its keeping our theme mode - lightTheme that is hardcoded for now) we can get the specific colors from theme object

const AppProvider = ({ children }) => {
  const [themeMode, setThemeMode] = useState("lightTheme");

  const value = { themeMode };
  const costumTheme = theme[themeMode];

  return (
    <AppContext.Provider value={value}>
      <ThemeProvider theme={costumTheme}>
        {children}
      </ThemeProvider>
    </AppContext.Provider>
  );
};

Perfect time to check if out theme provider it's working. For verify this really simply we can create a global file in styles folder and bring some of the theme colors here.

import { createGlobalStyle, css } from 'styled-components'

export default createGlobalStyle`
${({ theme }) => css`
  html {
    height: 100%;

    body {
      display: flex;
      flex-direction: column;
      height: 100%;
      margin: 0;

      #root {
        background: ${theme.colors.background};
        color: ${theme.colors.black};
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        font-family: sans-serif;
        height: 100%;
        padding: 15px;
      }
    }
  }
`}
`

Notice now that our theme colors are available in the entire application.
We can create an index.js file in the styles folder and export both global and theme files.

export { default as GlobalStyles } from "./global";
export { default as theme } from "./theme";

Once we bring GlobalStyles in AppProvider component and add it bellow the ThemeSwitcher our application background will take the styles corresponding to lightTheme

  • import Global styles in AppProvider
import { GlobalStyles, theme } from "./styles";
  • add global styles
 <ThemeProvider theme={costumTheme}>
      <GlobalStyles />
      {children}
 </ThemeProvider>

Now let's create a function for toggle the theme mode. We will check the previous state and change it base on the current mode

const toggleTheme = () => {
    setThemeMode(prevState => {
      if (prevState === 'lightTheme') {
        return 'darkTheme'
      } else {
        return 'lightTheme'
      }
    })
  }

Add this function in value object. After this the toggleTheme function will be available in the entire application

  const value = { toggleTheme, themeMode }

Final step is to bring this function in ThemeSwitcher component and execute it. Using Context API bring toggleTheme from the context and Switch component from react-switch.
Now all the magic its handle by Switch components. After reading the documentation we will know that it can receive props like as:

  • checked - receive true or false, we will check if the theme its light or dark;
  • height;
  • width;
  • checkedIcon - it can receive an icon, so we will import IoMdSunny for light icon from 'react-icons';
  • uncheckedIcon - it can receive an icon, so we will import IoMdMoon for light icon from 'react-icons';
  • onChange - invoked when the user clicks or drags the switch;
import React, { useContext } from "react";
import styled from "styled-components";
import { AppContext } from "./AppProvider";
import Switch from "react-switch";

const ThemeSwitcher = () => {
  const { toggleTheme, themeMode } = useContext(AppContext);
  const handleThemeChange = (e) => {
    toggleTheme();
  };
  return (
    <Root>
      <h1>Toggle Theme</h1>
      <Switch
        checked={themeMode === "lightTheme" ? true : false}
        className="test"
        height={50}
        width={120}
        checkedIcon={
          <Sun
            style={{
              display: "flex",
              justifyContent: "center",
              alignItems: "center",
              height: "100%",
              fontSize: 35,
              paddingLeft: 10,
            }}
            color={themeMode === "lightTheme" ? "white" : "grey"}
            className="light"
          />
        }
        uncheckedIcon={
          <Moon
            style={{
              display: "flex",
              justifyContent: "center",
              alignItems: "center",
              height: "100%",
              fontSize: 35,
              paddingLeft: 24,
            }}
            color={themeMode === "darkTheme" ? "blue" : "blue"}
            className="dark"
          />
        }
        onChange={handleThemeChange}
      />
    </Root>
  );
};

export default ThemeSwitcher;

const Root = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  .custom-classname .react-toggle--checked .react-toggle-track {
    background-color: red;
    height: 200px;
    width: 200px;
    padding: 30px;
  }
`;

For future improvements we can take advantage of the local storage and persist light/dark mode. We will make small changes in AppProvider component.

First we will need to check if there is a theme in localStorage or not, then set the initial themeMode with it, if not we will set it by default with lightTheme

const [themeMode, setThemeMode] = useState(
    localStorage.getItem("theme") || "lightTheme"
  );

Next we will bring useEffect and save the themeMode in localStore every time we change it

  useEffect(() => {
    localStorage.setItem("theme", themeMode);
  }, [themeMode]);

Add themeMode in the dependency array. The array is the second optional argument in the useEffect hook. As the name implies, it is an array of dependencies that, when changed from the previous render, will recall the effect function defined in the first argument

Conclusion

As this it's just a basic example how you can achieve this functionality, on a bigger application the complexity will increase and probably needs more work into it.

I would appreciate any feedback, good or bad, in order to improve the next articles.
Thanks for reading and happy coding! :)

Top comments (1)

Collapse
 
jakubsledz profile image
JakubSledz

Hey, thanks for this. It really helped me understand how context works. It will be easier to follow if you left some codesandbox link. Cuz for example it's hard to use your switcher without mentioning how react-icons work. Or maybe I missed smth?