DEV Community

Ravi Ojha
Ravi Ojha

Posted on

Build dark mode theme toggle in React

I got my inspiration for this post from Article By @murtuzaalisurti

Pre-requisite:

In this post we will build a dark mode theme toggle. This is what we are aiming for:

sun-moon

Nice, Let's start !!

1. Follow everything from Here (Also mentioned in pre-requisite) to start with react project from basic.

I just created a new folder sun-moon and copied everything from the previous post (react-from-scratch) except node_modules and did a fresh npm i.

2. Add css-loader and mini-css-extract-plugin as dependency:

npm i css-loader mini-css-extract-plugin -D
Enter fullscreen mode Exit fullscreen mode

This will allow us to import css in our js file, as shown below:

// in App.js file
import "./App.css";
Enter fullscreen mode Exit fullscreen mode

So, let's create App.css in src folder and import it in App.js.

3. Update webpack.config.js to include css-loader and mini-css-extract-plugin to this:

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, "./public"),
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: "babel-loader",
      },
      {
        test: /(\.scss|\.css)$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [new HtmlWebpackPlugin({ template: "./public/index.html" }), new MiniCssExtractPlugin()],
};
Enter fullscreen mode Exit fullscreen mode

4. Create Sun and Moon component

This is basically going to contain Sun/Moon svg as react component.

We can get Sun and Moon SVG from here:

Although, We can manually create React Component from SVG but we can also use this playground which will just convert the SVG to react component for us: React SVGR

Now, After doing all above, this is how our project will look like:

---sun-moon
  |--node_modules
  |--...
  |--src
     |--App.js
     |--App.css
     |--index.js
     |--Sun.js
     |--Moon.js
Enter fullscreen mode Exit fullscreen mode

5. Create DarkContext context

  • Create a file DarkContext.js in src folder, with this content:
import { createContext } from "react";

export const DarkContext = createContext(false);
Enter fullscreen mode Exit fullscreen mode
  • This creates a context object DarkContext using createContext method from React library.
  • DarkContext has the default theme value as false.

6. Wrap the App with DarkContext.Provider

  • Wrap App with DarkContext.Provider and specify the initial value.
  • Now, other components can access the value of the theme by using DarkContext.
  • For our example, Sun and Moon Component needs to know if current context is Dark or Light (not-dark).

Note: We could have passed the value of theme using props, and that would also be okay, but let's try to get a feel of react's context api.

So, after this our App.js will look like this:

import React, { useState } from "react";
import { DarkContext } from "./DarkContext";

import "./App.css";

const App = () => {
  const [isDark, setIsDark] = useState(false);

  return (
    <DarkContext.Provider value={isDark ? true : false}>
      <div className="instruction">Click On Sun/Moon &#128071;</div>
      <div className={`app${isDark ? " dark" : ""}`}>
        Hello World
      </div>
    </DarkContext.Provider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Here, App is also maintaining an internal state isDark, which helps to update our context value:

<DarkContext.Provider value={isDark ? true : false}>
...
</DarkContext.Provider>
Enter fullscreen mode Exit fullscreen mode

7. Create ThemeIcon component to hold Sun & Moon component:

import React, { useContext } from "react";
import Moon from "./Moon";
import Sun from "./Sun";

function ThemeIcon(props) {
  const { onClick } = props;

  return (
    <div className="theme-icon" onClick={onClick}>
      <Sun></Sun>
      <Moon></Moon>
    </div>
  );
}

export default ThemeIcon;
Enter fullscreen mode Exit fullscreen mode

Nothing fancy here, it's just a component which holds our Sun and Moon component, it also needs to have an onClick handler which will change the value of isDark from App's internal state and that has to be passed down from App component.

8. Include ThemeIcon component in App component and pass down the onClick handler:

import React, { useState } from "react";
import { DarkContext } from "./DarkContext";
import ThemeIcon from "./ThemeIcon";

import "./App.css";

const App = () => {
  const [isDark, setIsDark] = useState(false);

  return (
    <DarkContext.Provider value={isDark ? true : false}>
      <div className={`app${isDark ? " dark" : ""}`}>
        <ThemeIcon onClick={() => setIsDark(!isDark)}></ThemeIcon>
      </div>
    </DarkContext.Provider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

If we look at this line:

<div className={`app${isDark ? " dark" : ""}`}>
Enter fullscreen mode Exit fullscreen mode

And also take a look at some corresponding css:

.app {
  width: 100vw;
  height: 100vh;
  ...
}

.dark {
  ...
}
Enter fullscreen mode Exit fullscreen mode

We will notice that, We are saying html element div.app is going to be full width and full height. And whenever the value of isDark is true i am going to add css class dark as well.

9. Use context in Sun and Moon Component:

As for the useContext

  • It accepts a React Context object as an input. For us it will be DarkContext.
  • The output of useContext is the current value of the context (true/false).

So, we will use useContext to get the current value of the context in Sun/Moon component.

This is how we will be doing it:

const isDark = useContext(DarkContext);
Enter fullscreen mode Exit fullscreen mode

Our, Sun and Moon will now look like this:

import React, { useContext } from "react";
import PropTypes from "prop-types";
import { DarkContext } from "./DarkContext";

function Sun() {
  // Things to notice here:
  const isDark = useContext(DarkContext);

  return (
    <svg
      version="1.1"
      id="Capa_1"
      x="0px"
      y="0px"
      viewBox="0 0 457.32 457.32"
      // Things to notice here:
      className={`sun ${isDark ? " sun-dark" : ""}`}
    >
      ...
    </svg>
  );
}

export default Sun;
Enter fullscreen mode Exit fullscreen mode

It will exactly be same for Moon component as well.
Now, if we do:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

We should be able to see our app at http://localhost:8080/.

The only thing that we didn't discuss in entirety is it's css part, but if we are able to understand how sun is disappearing and how moon comes up, and how moon disappears and sun comes up, then we can say we understood it completely.

Although, we can see the css code here or in github (link shared at the end of this post) and try to understand what is going on, but instead let's look at these two screenshots, I believe that should be enough to understand the logic, rest all is just writing code for that logic.

So, to understand this, Let's look at this screenshot first, this is our initial state:
image

So, We see there is IconContainer (in terms or our code it is ThemeIcon component, that we created above), that is holding Sun icon/component, the opacity of sun is 1 and there we also see Moon but it's not going to be visible because it's opacity is 0 {opacity: 0}, when i click on IconContainer our Sun is going to be invisible by making its opacity as Zero(0) (by adding extra class sun-dark) and moon's opacity will become One(1) (by adding extra class, moon-dark to the moon svg, based on the value of isDark), we can have another look at Sun/Moon component and see how css classes are getting added.

When we click on IconContainer, this is what it will look like:
image

So, this is what happened when we clicked:

  • DarkContext's value is going to be true.
  • Sun/Moon is going to know that change, through context, based on that they are going to act.
  • Sun is going to attach sun-dark class to it's SVG, which will make it invisible (opacity: 0)
  • Moon is going to attach moon-dark class as well to it's SVG, which will make it visible (opacity: 1)

When we click again, same thing is going to happen just in reverse.

There are some transitions and transformation that I have added to make it more interesting, but not to worry, if we are able to understand the logic above, things will be easy.

We can see the live demo here: Sun Moon
We can see the entire code here

--Thanks, Ravi

Top comments (1)

Collapse
 
aburnsy profile image
Andy Burns

Really cool Ravi. Thanks for this!