How to create a dark\light mode switch in CSS and Javascript

In this tutorial, we'll take a look at how to create a dark theme for your web project, and how to switch from a default (light) theme to a dark one with the help of CSS Custom Properties.

To achieve this result, we are going to define all our project colors using CSS custom properties; and then, when a specific data attribute (or you can use a CSS class) is added to the body element, we update all these CSS variables with their new dark-theme values.

For example, we can use the color white as the default background-color for a component, and then change it to black when a data-theme="dark" is added to the body element:

:root {
  --color-bg: #ffffff;
}

.component {
  background-color: var(--color-bg);
}

[data-theme="dark"] {
  --color-bg: #000000;
}

When the data-theme="dark" is applied to the <body> element, the background-color of the .component changes from white to black.

Apply this same idea to all the colors of your web project, and you have a theme switcher! What's missing is just a toggle element that adds/removes the data-theme to the body element when it's checked.

Let's try this on a real case scenario #

We've put together a video tutorial that explains how to create the switch and apply a dark theme to a web project based on the CodyHouse Framework. Feel free to skip the video if you prefer to read the article.

👋 First time you hear about the CodyHouse Framework?

Step 1 of this tutorial is populating a web page with some content. To make it easier to follow along, we're going to use the CodyHouse framework.

Download the framework on Github and launch it. To do so, navigate to the framework folder you have just downloaded, and run the following commands:

npm install

npm run gulp watch

Once the new project is up and running, we can pick some of the CodyHouse components and add them to the index.html file. If you prefer to skip this step, you can download the project files of this tutorial on Github.

The advantage of using the CodyHouse components is that they have already been built using CSS Variables; updating the value of the color variables (as explained in the first part of the article) will automatically update their styles with no additional changes needed.

Here's the list of the components used to create our final project:

Make sure to include the HTML of these components in the index.html and import the SCSS style as well.

☝️ Tip: to organize the SCSS files of the CodyHouse components, we suggest to add a separate 'components' folder and create, inside it, a separate SCSS file for each one of them. You can then import all these files directly in your style.scss (or inside a _components.scss file). The result will be something like this:

Files organization

If you have selected components that have a Javascript file, make sure to include that one as well.

Here's the final result once all the components are in place:

preview of the final result

We still need to:

  1. Create a dark theme
  2. Add a switch to the page

To create a dark theme, we can use the Colors Editor. Click on the '+' icon next to the 'Themes' label and rename the new theme 'dark'.

create a new color theme

Select the Bg + Contrast scale, and pick a dark color for the --color-bg, and a light color for the --color-contrast-higher. The intermediate values are automatically generated by the editor. To preview the new theme, click on the [Preview theme] switch located on top of the editor.

If you click on the [View code] button (header of the editor), you'll notice the editor has created a data-theme="dark" attribute, where it updates the values of the color variables. You can do the same process manually if you prefer.

new theme code

⚠️ Note: don't worry if you see the color variables defined using the defineColorHSL function (it does not affect the theme creation); we need this to combine SASS color functions with CSS Variables.

Make sure to replace the content of the _colors.scss global with the new code generated by the Editor.

Now keep in mind that all the components have been built using the color variables. For example, in the Main Header component, we have:

.main-header {
  background-color: var(--color-bg);
  border-bottom: 1px solid var(--color-contrast-low);
}

.main-header__nav-label { // menu label
  color: var(--color-contrast-medium);
}

.main-header__nav-link { // link within list item
  color: var(--color-contrast-higher);

  &[aria-current] { // style of selected link
    color: var(--color-primary);
  }
}

Because of that, when you apply data-theme="dark" to the body, the values of the color variables are updated, and the new theme style is automatically applied.

The last step is including the switch component.

Include the switch component
Include the switch component

In your JS file, you'll need to add this snippet (in the project file, we've already included this script in the main.js file):

document.getElementById('themeSwitch').addEventListener('change', function(event){
  (event.target.checked) ? document.body.setAttribute('data-theme', 'dark') : document.body.removeAttribute('data-theme');
});

This script listens for changes in the input status and, according to its checked value, adds or removes the data-theme attribute to the body element.

With the switch in place, you have all you need to turn on/off your dark theme! 🙌

Feel free to download the final project on Github.

Dark switch preview

What about the browser's support? #

Although we use a gulp plugin to generate a fallback for CSS variables, themes other than the default one won't be visible in older browsers. That shouldn't be an issue because browsers that don't support CSS variables will still render the default theme (the content is accessible).

More info on our Colors documentation page.

How to save theme selection using the Local Storage API? #

In this follow up article, we explain how you can use the Local Storage API to save the theme selection so that it's preserved while navigating the website.

If you have suggestions on what we could improve, get in touch! Any feedback is welcome.