Here we will learn how to implement a dark mode in React or Gatsby with no dependancies or plugins.
To do that, we will use CSS variables. Thay have support from all major browsers except IE11, so if you need to work in IE you just need to have a little more CSS styles.
Our styles should look similar to the layout.scss
example. We have two classes light
and dark
which we will toggle. So all colours will be light with the light
class and dark with the dark
class. If you prefer to not use variables, you will need to organize all colour related rules into the two classes. The main difference is that with CSS variables will be easier to maintain and scale the project.
layout.scss
body {
color: var(--primary);
background-color: var(--secondary);
&.light {
--primary: black;
--secondary: white;
}
&.dark {
--primary: white;
--secondary: black;
}
}
We will isolate all logic for changing modes in one component. In our case the component will be DarkModeSwitch
.
const [mode, setMode] = useState(() => {
// check for Gatsby and NodeJS
if (typeof window !== 'undefined') {
const val = localStorage.getItem('theme');
return val ? JSON.parse(val) : themeType.light;
}
return themeType.light;
});
Here we use a simple useState Hook to toggle between modes. In our case we use localStorage to preserve users' choice. You will notice the typeof window !== 'undefined'
check, it's there because I use Gatsby and when I build the project through NodeJS the global object is not the window
. For all of you who doesn't use NodeJS to build the project the check will be unnecessary.
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('theme', JSON.stringify(mode));
}
}, [mode]);
Here in the useEffect Hook all the magic happens. We pass as a second argument an array with mode
variable. This way we have a way to execute logic every time the mode
is changing. And when we change the theme mode
we save the value in the localStorage
.
Next, we have the toggle method:
const toggleMode = () => {
setMode(mode === themeType.light ? themeType.dark : themeType.light);
};
To change classes in the <body>
tag below we use React Helmet package. If in your project you don't want another dependency you can use pure JavaScript to select the DOM element, for example document.body.classList
.
<Fragment>
<Helmet>
<body className={mode} />
</Helmet>
<div className="theme-mode" onClick={toggleMode} role="button" tabIndex="0" onKeyDown={toggleMode}>
{mode === themeType.light ? <img src={moon} alt="dark mode" /> : <img src={sun} alt="light mode" />}
</div>
</Fragment>
Below is the final code. I have always liked to keep my projects with fewer dependancies. This way my project is less bloated and I learn how things work.
Final code for dark-mode-switch.js
import React, { Fragment, useState, useEffect } from 'react';
import Helmet from 'react-helmet';
import moon from '../images/moon.svg';
import sun from '../images/sun.svg';
import '../styles/dark-mode-switch.scss';
const themeType = {
dark: 'dark',
light: 'light'
};
const DarkModeSwitch = () => {
const [mode, setMode] = useState(() => {
if (typeof window !== 'undefined') {
const val = localStorage.getItem('theme');
return val ? JSON.parse(val) : themeType.light;
}
return themeType.light;
});
useEffect(() => {
if (typeof window !== 'undefined') {
localStorage.setItem('theme', JSON.stringify(mode));
}
}, [mode]);
const toggleMode = () => {
setMode(mode === themeType.light ? themeType.dark : themeType.light);
};
return (
<Fragment>
<Helmet>
<body className={mode} />
</Helmet>
<div className="theme-mode" onClick={toggleMode} role="button" tabIndex="0" onKeyDown={toggleMode}>
{mode === themeType.light ? <img src={moon} alt="dark mode" /> : <img src={sun} alt="light mode" />}
</div>
</Fragment>
);
};
export default DarkModeSwitch;