Light and Dark Mode Feature for NextJs/CSS variables

ยท

5 min read

Table of contents

No heading

No headings in the article.

Adding the Light/Dark mode toggle feature using NextJs is a classic web development case of a seemingly simple concept having extra layers of implementation that borders on complication. It is not complicated in the entire sense of the word - complication - but it is surprisingly close.

In this article, we shall attempt a confrontation of the concept, with the sole aim of dissolving it in the waters of simplicity. Let us dive head-on!

First, we install the NextJs app, light-dark-mode using the syntax:

npx create-next-app@latest light-dark-mode

Upon successful installation of our next application, one of the folders called the pages folder will contain the index.js file, _document.js file, _app.js file and the style folder will contain the global.css file amongst other files. We can proceed to delete the other files in the styles folder as the global.css file is the only one we need.

We are going to implement the light/dark modes toggle on a Navbar component. So we could go ahead to create a component folder and Navbar.jsx within the component folder.

import React from "react"

const Navbar = () => {
    return (
        <div>
        <form action="#">
          <label className="switch">
            <input
              type="checkbox"
              checked=""
              onChange=""
            />
            <span className="slider"></span>
          </label>
        </form>
      </div>
)
}

Our Navbar component returns input type of "checkbox" which will be the switch for the light and dark modes. The elements in our Navbar have the `className` attributes that will come forth from the global.css file.

We shall now go over to the global.css file in our styles folder and write the css that populates the className attributes in our Navbar component and also, the reference root css that will toggle between our light and dark modes.

:root {
  --color-primary-accent: #2614c7;
  --color-primary: #000;
  --color-button: #3d486c;
  --color-paragraph: #797979;
  --color-button-text: rgb(255, 255, 255);
  --color-background: #fff;
  --color-anchor: #000aff;
}

[data-theme="dark"] {
  --color-primary-accent: #00ff4c;
  --color-primary: #fff;
  --color-button: #00ff4c;
  --color-paragraph: #8a8a8a;
  --color-button-text: rgb(0, 0, 0);
  --color-background: #22212b;
  --color-anchor: #00ff4c;
}

body {
  background-color: var(--color-background);
  color: var(--color-primary);
  font-size: 16px;
  font-family: "Poppins", sans-serif;
}
.switch {
  position: relative;
  display: inline-block;
  width: 60px;
  height: 34px;
}

.switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

.slider:before {
  position: absolute;
  content: "";
  height: 26px;
  width: 26px;
  left: 4px;
  bottom: 4px;
  background-color: white;
  -webkit-transition: 0.4s;
  transition: 0.4s;
}

input:checked + .slider {
  background-color: #33f321;
}

input:focus + .slider {
  box-shadow: 0 0 1px #33f321;
}

input:checked + .slider:before {
  -webkit-transform: translateX(26px);
  -ms-transform: translateX(26px);
  transform: translateX(26px);
}

.slider {
  border-radius: 34px;
}

.slider:before {
  border-radius: 50%;
}

For this styles to work globally as the name implies, we must import it into the _app.js file.

import "../styles/globals.css";

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

The next step to take is to head over to the document.js file in the pages folder. This file is only rendered on the server, so event handlers like onClick cannot be used in _document. This file wraps our entire application.

import { Html, Head, Main, NextScript } from 'next/document'

 function Document() {
  return (
    <Html>
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  )
}

One of the major reasons why this simple toggle feature borders on complication is the fact that we have to set the css theme `[data-theme="dark"]` to the localStorage . We would write some functions which includes the setInitialColorMode() which will contain the getInitialColorMode() which gets the set theme from the local storage and also the themeInitializerScript() which will be set to the script element's dangerouslySetInnerHTML attribute.

import { Html, Head, Main, NextScript } from "next/document";

function Document() {
  function setInitialColorMode() {
    // Checking the initial color preference
    function getInitialColorMode() {
      const persistedPreferenceMode = window.localStorage.getItem("theme");
      const hasPersistedPreference =
        typeof persistedPreferenceMode === "string";

      if (hasPersistedPreference) {
        return persistedPreferenceMode;
      }

      // Check the current preference
      const preference = window.matchMedia("(prefers-color-scheme: dark)");
      const hasMediaQueryPreference = typeof preference.matches === "boolean";

      if (hasMediaQueryPreference) {
        return preference.matches ? "dark" : "light";
      }

      return "light";
    }

    const currentColorMode = getInitialColorMode();
    const element = document.documentElement;
    element.style.setProperty("--initial-color-mode", currentColorMode);

    // If darkmode apply darkmode
    if (currentColorMode === "dark")
      document.documentElement.setAttribute("data-theme", "dark");
  }

  //creating the theme initializer function
  const themeInitializerScript = `(function() {
    ${setInitialColorMode.toString()}
    setInitialColorMode();
})()
`;
  return (
    <Html lang="en">
      <Head>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link
          rel="preconnect"
          href="https://fonts.gstatic.com"
          crossOrigin="true"
        />
        <link
          href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300&family=PT+Serif:ital@1&family=Roboto+Mono:wght@100&display=swap"
          rel="stylesheet"
        />
      </Head>
      <body>
        <script
          dangerouslySetInnerHTML={{ __html: themeInitializerScript }}
        ></script>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

export default Document;

That completes it for the logic we have to implement in our _document.js file.

Now we go over to our Navbar.jsx file in the components folder, where we will use the React's useState and useEffect hooks to effectively manage the toggle states of the light and dark modes and also set the css theme to the local storage respectively.

import React, {useState, useEffect} from "react"

const Navbar = () => {
    //to manage the change in state of the themes
  const [darkTheme, setDarkTheme] = useState(undefined);

  //function to handle the toggle between light and dark themes
  const handleToggle = (event) => {
    setDarkTheme(event.target.checked);
  };

  //the effect hook to handle the (re)rendering of the the themes
  useEffect(() => {
    if (darkTheme !== undefined) {
      if (darkTheme) {
        document.documentElement.setAttribute("data-theme", "dark");
        window.localStorage.setItem("theme", "dark");
      } else {
        document.documentElement.removeAttribute("data-theme");
        window.localStorage.setItem("theme", "light");
      }
    }
  }, [darkTheme]);

  //another effect hook to handle the initial-color-mode
  useEffect(() => {
    const root = window.document.documentElement;
    const initialColorValue = root.style.getPropertyValue(
      "--initial-color-mode"
    );
    setDarkTheme(initialColorValue === "dark");
  }, []);

    return (
        <div>
        <form action="#">
          <label className="switch">
            <input
              type="checkbox"
              checked={darkTheme}
              onChange={handleToggle}
            />
            <span className="slider"></span>
          </label>
        </form>
      </div>
)};

I took the liberty of setting our initial color mode to be the dark mode.

This in effect, brings the not so complicated implementation to a simple end ๐Ÿ˜ฎโ€๐Ÿ’จ

Thank you!

ย