There are many ways to implement a light/dark theme feature on your website. But how can you create a clean solution that will be easy to use and maintain overtime?
In this article, I will show you how to create a light/dark theme toggle functionality using React and Sass.
I have created a demo project based on the popular TV show Rick and Morty. There are a couple of pages dedicated to the main characters all designed in dark theme.
I will walk you through how to add light theme styles, and how to toggle between the two themes. You can then use this light theme solution in your own projects.
Table of Contents
- Prerequisites
- Installation steps for the Demo App
- How does the light/dark theme toggle work?
- How to install the useDarkMode hook
- Creating a custom useTheme hook
- Creating the light/dark theme toggle button
- Adding the useTheme hook to all the pages
- How to add a Sass map for the light/dark theme styles
- Applying the themes to the individual stylesheets
Prerequisites
This article assumes you have a basic fundamental knowledge of React, Sass, and the command line.
This demo project is using Yarn, so it is recommended that you install Yarn.
Installation steps for the Demo App
- Clone the project
git clone https://github.com/jdwilkin4/Light-Dark-Theme-Starter-Code.git
cd
into theLight-Dark-Theme-Starter-Code
directory
cd Light-Dark-Theme-Starter-Code
- Install the dependencies
yarn install
- Start the local server
yarn start
You should see the homepage with two links that will lead you to the Rick and Morty pages.
How does the light/dark theme toggle work?
We will be creating a button where users can select if they prefer dark or light mode, and this button will toggle between the two styles. By default, the initial setting will be for dark mode.
When the user refreshes the page, their theme preference will be saved in local storage.
How to install the useDarkMode hook
We will be using an npm package called use-dark-mode which is a custom hook used to implement the toggle functionality between light and dark mode.
Keep your server running, open up a new tab in the terminal, and run the command yarn add use-dark-mode
.
Creating a custom useTheme hook
The goal of this hook is to return a string value of either light-mode
or dark-mode
based on the current mode we are in. We will then use this string value as a class and apply it to the JSX elements.
Open up your code editor, locate the src
folder and create a new folder called utils
. Inside the utils
folder, create a new file called useTheme.js
.
At the top of your useTheme.js
file, include the React and useDarkMode
imports.
import React from "react";
import useDarkMode from "use-dark-mode";
Underneath those imports, add these two variables:
const lightTheme = "light-mode";
const darkTheme = "dark-mode";
Below the variable declarations, you will create the useTheme
hook.
export const useTheme = () => {};
Inside the useTheme
hook, we want to include the useDarkMode
hook and assign it to a const variable called darkMode
.
const darkMode = useDarkMode();
The return value for the useDarkMode()
hook is an object, and one of the property names we are going to use is called value
. The value
property is a boolean which represents if dark mode is on or not.
Next, we want to add a new state variable and assign it the dark theme value.
const [theme, setTheme] = React.useState(darkTheme);
We are then going to add a useEffect
hook and update the theme based on each time the mode changes.
React.useEffect(() => {
setTheme(darkMode?.value ? darkTheme : lightTheme);
}, [darkMode.value]);
We are adding darkMode.value
to the dependency array because we want it to only re-run the effect when the value changes on re-render.
Lastly, we want to return our theme.
return theme;
This is what the entire useTheme
hook should look like.
export const useTheme = () => {
const darkMode = useDarkMode();
const [theme, setTheme] = React.useState(darkTheme);
React.useEffect(() => {
setTheme(darkMode?.value ? darkTheme : lightTheme);
}, [darkMode.value]);
return theme;
};
Creating the light/dark theme toggle button
Locate the src/components
folder, and create a file ThemeBtn.js
and a ThemeBtn.scss
file.
Inside that file, add the imports for React, useDarkMode
and useTheme
.
import React from "react";
import useDarkMode from "use-dark-mode";
import { useTheme } from "../utils/useTheme";
Right underneath those imports, include your stylesheet for this button component.
import "../components/ThemeBtn.scss";
Now, we are going to create our Button component.
const ThemeBtn = () => {};
export default ThemeBtn;
Inside the ThemeBtn
component, we are going to use the useDarkMode
hook and set the value to true because we want the default to be set to dark mode.
const darkMode = useDarkMode(true);
We are also going to create a variable called theme and assign to it the useTheme
hook.
const theme = useTheme();
Inside the return, we are going to create a button. Since darkMode
is an object that has a property called toggle
, we can use that in the onClick
function to toggle between light and dark themes.
For the button text, we will create a ternary operator which will show the text of "Light mode" or "Dark mode" depending on the state of the theme.
return (
<button className="btn-theme" type="button" onClick={darkMode.toggle}>
{theme === "dark-mode" ? "Light mode" : "Dark mode"}
</button>
);
In order to see our toggle button in action, we need to add it to one of the pages. Most people choose to add the toggle button to the navigation bar. For our demo project, we will add it to the App.js
file.
Import the ThemeBtn
into the App component, and add the <ThemeBtn />
just before the routes.
import ThemeBtn from "./components/ThemeBtn";
function App() {
return (
<>
<ThemeBtn />
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/rick" element={<RickSanchezPage />} />
<Route path="/morty" element={<MortySmithPage />} />
</Routes>
</>
);
}
You should now see the button in the browser. Try clicking on it and see the text change between light and dark mode.
Let's add some styling to our button.
Open up the ThemeBtn.scss
file and add these styles for the btn-theme
class.
@import "../styles/colors";
.btn-theme {
background-color: $purple100;
border: none;
color: $grey100;
display: block;
font-size: 1.2rem;
font-weight: 600;
width: 150px;
padding: 5px;
text-align: center;
margin: 0;
cursor: pointer;
&:hover {
background-color: $purple200;
}
}
Adding the useTheme hook to all the pages
We need to import the useTheme
hook to all of our pages because we want to apply the dark and light mode classes to the JSX elements.
Inside the App.js
file, import the useTheme
hook.
import { useTheme } from "./utils/useTheme";
Inside the App component, create a variable called theme
, and assign the hook to it.
const theme = useTheme();
Replace the empty React fragments with div
elements, and apply the theme
variable to the className
.
<div className={theme}>
<ThemeBtn />
<Routes>
<Route path="/" element={<Homepage />} />
<Route path="/rick" element={<RickSanchezPage />} />
<Route path="/morty" element={<MortySmithPage />} />
</Routes>
</div>
For the Button.js
file, import the useTheme
hook, and create the theme
variable like before. Then, add that variable to the className
.
import { useTheme } from "../utils/useTheme";
export const Button = ({ text, path }) => {
const theme = useTheme();
return (
<Link to={path} className={`btn ${theme}`}>
{text}
</Link>
);
};
For the CharacterTemplate.js
file, import the useTheme
hook and create the theme
variable like before. Then add that variable to the className
for the div elements.
// here is the full JSX markup
<div className={theme}>
<h1>{title}</h1>
<Button text="Return Home" path="/" />
<div className="flex-container">
{characterInfo.map((character, id) => (
<div key={id} className="character-container">
<h2>{character.name}</h2>
<img src={character.image} alt="character avatar" />
</div>
))}
</div>
</div>
How to add a Sass map for the light/dark theme styles
Inside the styles
folder, open up the colors
file and add the $grey200: #f5f1f1;
variable.
This is what the complete colors file should look like.
$blue700: #1a1a40;
$blue600: #2c2c66;
$black: #000;
$grey100: #fdfcfc;
$grey200: #f5f1f1;
$purple100: #7a0bc0;
$purple200: #650c9d;
Inside the styles
folder, create a new file called _light-dark-theme.scss
.
At the top of your Sass file, import the colors file.
@import "./colors";
Then, we are going to create a new Sass map called themes
.
$themes: ();
Inside the themes
map, we are going to add individual maps for the background and text colors used for the light and dark themes.
$themes: (
bgThemeColor1: (
darkTheme: $blue700,
lightTheme: $grey100
),
bgThemeColor2: (
darkTheme: $blue600,
lightTheme: $grey200
),
textThemeColor1: (
darkTheme: $grey100,
lightTheme: $black
)
);
We are now going to create a mixin called styles
with an argument called $mode
. This mixin will be used later on in the dark-mode
and light-mode
classes.
@mixin styles($mode) {
}
Inside the mixin, we are going to create an @each
rule that will iterate through each key value pair in the themes
map.
@each $key, $map in $themes {
}
The $key
represents each of the background and text colors we created (Ex. bgThemeColor1
). The $map
represents each of the values.
For example:
(
darkTheme: $blue700,
lightTheme: $grey100,
)
Inside that @each
rule, we are going to create another rule that iterates over each key/value pair for the individual maps.
@each $prop, $color in $map {
}
Inside that @each
rule, we will create a condition that checks which mode we are in and applies the appropriate style to that class.
@if $prop == $mode {
--#{$key}: #{$color};
}
The reason why we are adding the --
in front of the key, is because we want to reference these color variables in the individual stylesheets using CSS variable syntax.
For example:
var(--color)
This is what the complete mixin should look like.
@mixin styles($mode) {
@each $key, $map in $themes {
@each $prop, $color in $map {
@if $prop == $mode {
--#{$key}: #{$color};
}
}
}
}
Below the mixin, we are going to add the light and dark theme styles to the appropriate classes using the @include
rule.
.dark-mode {
@include styles("darkTheme");
}
.light-mode {
@include styles("lightTheme");
}
This is what the entire light-dark-theme
file should look like.
@import "src/styles/colors";
$themes: (
bgThemeColor1: (
darkTheme: $blue700,
lightTheme: $grey100,
),
bgThemeColor2: (
darkTheme: $blue600,
lightTheme: $grey200,
),
textThemeColor1: (
darkTheme: $grey100,
lightTheme: $black,
),
);
@mixin styles($mode) {
@each $key, $map in $themes {
@each $prop, $color in $map {
@if $prop == $mode {
--#{$key}: #{$color};
}
}
}
}
.dark-mode {
@include styles("darkTheme");
}
.light-mode {
@include styles("lightTheme");
}
Applying the themes to the individual stylesheets
Inside the App.scss
file, import the light-dark-theme
file.
@import "./styles/light-dark-theme";
We are going to replace the background and text colors with the variables we created earlier.
body {
background-color: var(--bgThemeColor1);
color: var(--textThemeColor1);
text-align: center;
}
If you test out the light/dark theme toggle button, you will notice that the background and text colors will change.
It would be nice if there were a gradual transition between the two colors. We can accomplish this by using the CSS transition property.
body {
background-color: var(--bgThemeColor1);
color: var(--textThemeColor1);
text-align: center;
transition: background-color 0.5s ease;
}
Inside the CharacterTemplate.scss
file, import the light-dark-theme
file.
@import "../styles/light-dark-theme";
Then replace the background and text colors with the CSS variables we created earlier.
.character-container {
color: var(--textThemeColor1);
background-color: var(--bgThemeColor2);
Go to the browser and test out the light/dark theme button. You should be able to see both themes.
Conclusion
We have successfully created a light/dark theme solution using React and Sass.
You can implement this solution into your own projects and it will be easy to scale and maintain over time.
Here is the final demo project and source code.