How to Add Dark Mode with CSS and a Touch of JS


3 min read

What We Will Be Building

Setting Up the CSS Variables

First, we'll have to set up some global CSS Variables. I like to define two different types of variables: we'll use one type to explicitly refer to individual colours and we'll use the other type for our themes.

Explicit Colour Variables

As a starting point, let's steal some colours and variable names from Tailwind CSS. If we had to name them ourselves, we will name these according to their colour.

:root {
  --color-lavender-100: #f3f3f7;
  --color-lavender-300: #b6b6ce;
  --color-lavender-700: #2b2d42;
  --color-lavender-900: #101119;

Light Theme Variables

Now, let's define the variables for our light theme. I've put these inside the :root selector and this will become our default theme. I like to use generic names like primary and secondary.

Think of these as referring to specific functions rather than specific colours.

:root {
  /* Explicit Variables as Per Above */

  /* Light Mode Colours */
  --background: var(--color-lavender-100);
  --primary: var(--color-lavender-900);
  --secondary: var(--color-lavender-700);
  --button-primary-text: var(--color-lavender-100);
  --button-secondary-text: var(--color-lavender-700);

  background-color: var(--background);

Dark Theme Variables

Let's define our dark mode variables. This time, we'll put these inside the [color-theme="dark"] selector.

The [color-theme="dark"] selector will apply when the color-theme attribute in the document element is set to "dark". This is a custom attribute that we will set later in this tutorial.

/* Dark Mode Colours */
[color-theme="dark"] {
  --background: var(--color-lavender-900);
  --primary: var(--color-lavender-100);
  --secondary: var(--color-lavender-300);
  --button-primary-text: var(--color-lavender-700);
  --button-secondary-text: var(--color-lavender-100);

Just to keep things simple for this tutorial, I have reversed the colours.


Well done for making it here! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

Let's take a short break and recap what we have done so far.

  1. Declared some CSS variables with names that explicitly refer to colours.
  2. Declared some more CSS variables. We named these according to their function and we will use these for our default light theme.
  3. Define our dark theme inside the [color-theme="dark"] selector which would target a custom document attribute. The browser will conditionally override the theme if this attribute is set to "dark".

Let's Sprinkle In Some JavaScript.

In order for dark mode to work, we just need to set our custom document attribute. We'll achieve this via the document.documentElement.setAttribute() method.

In this example, I'll be using React, but you can use another framework or even Vanilla JS. Feel free to use the weapon of your choice. The main idea is just to set the attribute via method above.

Depending on what you are using, you might have to use document.documentElement.getAttribute().

import { useState, useEffect } from "react";
import "./styles.css";

// Sets Default Theme Based on Browser Preferences
const initialTheme = window.matchMedia("(prefers-color-scheme: dark)").matches;

export default function App() {
  const [darkMode, setDarkMode] = useState(initialTheme);

  useEffect(() => {
      darkMode ? "dark" : "light"
  }, [darkMode]);

  const toggleDarkMode = () => {

  // JSX
  return ...

Homework Time

In this tutorial, I have only covered theming with two options.

Over to you. How would you implement three or more themes?