Light and Dark Mode Feature for NextJs/CSS variables
Table of contents
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!