How to Add Dark Mode To Gatsby App

By Henri Parviainen

Gatsby app dark mode

Building dark mode by leveraging CSS variables is quite straightforward in a basic React app. However, when it comes to Gatsby, there are some unique hacks you need to know to succeed in creating a dark mode for your application. In this post, we will go through the process of creating dark mode for a Gatsby app from start to finish.

We will create a dark mode functionality to Gatsby app using only plain CSS, React's UseState hook, and some APIs provided by Gatsby.

Let's get started!

First, we will go through the whole implementation in general so you can get an idea of how we approach the problem. Then we will go through the process step by step so you'll be able to implement your own dark mode after reading the post.

The plan is to take advantage of CSS variables and React's UseState hook. Any other state manager (Redux, Context API, etc.) will work as well, but for simplicity's sake, we'll just use the standard UseState hook.

Here is what we need to implement:

  1. CSS variables with values for dark and light mode
  2. Switch button so users can change between light and dark mode
  3. Add wrapPageElement function using Gatsby browser API and Gatsby SSR API. This will let us keep a consistent theme when changing between pages.

1. Adding CSS variables

We can use CSS variables to create custom themes in our app. Let's add our variables with our default (light theme) values to the body selector.

index.css

body {
  --color-background: #fff;
  --color-text: #000;
}

By adding these variables to the body element, they are globally available for all our elements and classes.

Next, we'll want to add values for the dark mode variables. We will do this by creating a class .dark for dark mode:

index.css

.dark {
  --color-background: #000;
  --color-text: #fff;
}

We can use the variables in our CSS like this:

index.css

body {
  background-color: var(--color-background);
}

h1 {
  color: var(--color-text);
}

Now, we have defined our themes for light and dark modes.

2. Adding a switch button to swap between themes

Next, we need a button to switch between dark and light themes.

We'll have a Header component that is shown in all of our pages, so it makes sense to add the switch button in our Header component.

Here is how the Header component looks like.

import React, { useState, useEffect } from "react";

import Sun from "../assets/sun.svg";
import Moon from "../assets/moon.svg";

// You'll need to import index.css just in one of the components in your app.

import "../styles/index.css";

const Header = props => {
  const [isDark, setIsDark] = useState(false);

  useEffect(() => {
    if (isDark) {
      document.body.classList.add("dark");
    } else {
      document.body.classList.remove("dark");
    }
  }, [, isDark]);

  return (
    <div>
      {isDark ? (
        <img onClick={() => setIsDark(!isDark)} src={Sun} />
      ) : (
        <img onClick={() => setIsDark(!isDark)} src={Moon} />
      )}
    </div>
  );
};

export default Header;

Sun and Moon are SVG icons from Lucide icons. Make sure you save them in /assets directory.

These icons work as buttons and when clicked they will eighter add or remove the .dark class from body and thus change the theme in our app.

3. Adding wrapPageElement hook

Now, our dark mode would already work on a single page, but if we change pages the application will lose the track of the current theme. We need to change this so the theme won't change when we switch between pages.

We will do this by using Gatsbys wrapPageElement function provided by Gatsby browser API and SSR API.

With wrapPageElement function we can set wrapper components around components that won’t get unmounted on page changes.

Let's add the following code to gatsby-browser.js file.

gatsby-browser.js

import React from "react";
import HeaderWrapper from "./src/components/headerWrapper";

export const wrapPageElement = ({ element, props }) => (
  <HeaderWrapper {...props}>{element}</HeaderWrapper>
);

HeaderWrapper is just a wrapper component that will wrap our header component. We will need to create this component as well.

HeaderWrapper.js

import React from "react";
import Header from "./header";

const HeaderWrapper = ({ children }) => (
  <div>
    <Header />
    {children}
  </div>
);

export default HeaderWrapper;

Gatsby docs recommend that when we use wrapPageElement on browser API, we should also use the equivalent hook in Gatsby SSR API.

So, we will need to create gatsby-ssr.js file in our apps root directory where we will paste the same code that we added to gatsby-browser.js.

gatsby-ssr.js

import React from "react";
import HeaderWrapper from "./src/components/headerWrapper";

export const wrapPageElement = ({ element, props }) => (
  <HeaderWrapper {...props}>{element}</HeaderWrapper>
);

This will make sure that when we change pages in our app, our server keeps in sync with the changes in the browser.

Final thoughts

That's all you need to do to create a working dark mode for your Gatsby app.

If you want to take a deeper look into the code. I have created a Github repo with the dark mode implemented as explained in this tutorial.

Thanks for reading and hopefully you found this article useful!

SHARE