'use strict';

How I added a Dark theme to my blog

August 25, 2020·5 min read

The very first thing that I really liked about Dan Abramov’s blog is the dark-light mode toggle. Firstly, because it’s a fun little thing to have both for developers and for their readers and Secondly, implementing the toggle is not that straight forward as it may seem in a Gatsby blog.

So, with this post, my aim is to explain “how I” (to be honest - “how Dan”) implemented the toggle.

dark-light-toggle

Goals

  1. Add a toggle switch/button to change the theme between dark mode or light mode. I used the react-toggle library for that. It’s open source (Kudos to it’s contributors!) and I’ll provide a link to the github repo at the end of this post.
  2. We’ll also persist the reader’s preference inside of browser’s localstorage so they don’t have to toggle everytime they re-visit our site.
  3. Use Gatsby’s default-html.js to inject our javascript when our website loads.

A side note

There are few other ways to implement dark mode in gatsby and one such way is by using Gatsby’s inbuilt dark mode plugin (link at the end). But what we’ll be doing in this post is to basically use gatsby’s default-html.js to inject our javascript which will essentially add a css class ('dark' or 'light') to our body element, we’ll then colour our website’s background and text according to the class. Now, you may also use gatsby-ssr.js to add your custom script but I’m going to go old school and use default-html.js to do the same instead. And don’t worry, I’ll add some more references at the end of post if you wish to try doing it the other way.

Implementation

  1. Copy the default-html.js file at .cache/default-html.js inside your src directory and rename it as html.js.
  2. We’ll then add a script inside of the html.js file we just created, which will do the magic of adding the appropriate css class - 'dark' or 'light' in html body when our website is loaded for the first time.
//html.js
import React from "react";
import PropTypes from "prop-types";

export default function HTML(props) {
  return (
    <html {...props.htmlAttributes}>
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="x-ua-compatible" content="ie=edge" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        />
        {props.headComponents}
      </head>
      <body {...props.bodyAttributes} className="light">
        <script
          dangerouslySetInnerHTML={{
            __html: `
          (function() {
            window.__onThemeChange = function() {};
            function setTheme(newTheme) {
              window.__theme = newTheme;
              preferredTheme = newTheme;
              document.body.className = newTheme;
              window.__onThemeChange(newTheme);
            }

            var preferredTheme;
            try {
              preferredTheme = localStorage.getItem('theme');
            } catch (err) { }

            window.__setPreferredTheme = function(newTheme) {
              setTheme(newTheme);
              try {
                localStorage.setItem('theme', newTheme);
              } catch (err) {}
            }

            var darkQuery = 
                 window.matchMedia('(prefers-color-scheme: dark)');
            darkQuery.addListener(function(e) {
              window.__setPreferredTheme(e.matches ? 
                                         'dark' : 'light')
            });

            setTheme(preferredTheme || (darkQuery.matches ? 
                                        'dark' : 'light'));
          })();
          `,
          }}
        />
        {props.preBodyComponents}
        <div
          key={`body`}
          id="___gatsby"
          dangerouslySetInnerHTML={{ __html: props.body }}
        />
        {props.postBodyComponents}
      </body>
    </html>
  );
}

The only thing we have added to the existing (code) in default-html.js is our script tag and the code inside of it. And needless to say that we have to use dangerouslySetInnerHTML to do so because, well, that’s the React way!

The setTheme function’s main reponsibility is to apply an appropriate class to the html body.

  1. Below is the css which we’ll use for the color change to actually take effect (offcourse we haven’t yet added our toggle switch which will trigger the changes.)
body {
  --blk: rgb(0, 0, 0);
  --blu: #007acc;
  background-color: var(--bg);
}

body.light {
  --bg: #ffffff;
  --bg-secondary: rgb(249, 250, 251);
  --header: var(--blk);
  --textNormal: #222;
  --textTitle: #222;
  --textLink: var(--blk);
}

body.dark {
  -webkit-font-smoothing: antialiased;
  --bg: #282c35;
  --bg-secondary: rgb(54, 60, 72);
  --header: #ffffff;
  --textNormal: rgba(255, 255, 255, 0.88);
  --textTitle: #ffffff;
  --textLink: var(--blu);
}

The most important thing to note in the css is that we are using css variables and more importantly the background color for body is background-color: var(--bg); where, as you can see, the --bg variable changes with the current css class in effect.

  1. Next, we’ll use the react-toggle library to add the toggle switch. I’ll add the link to it’s github repo where you can get the code and css for the same. I added this component inside of my layout.js file which usually is provided out of the box if you’re using a starter.
<Toggle
  icons={{
    checked: (
      <img
        src={moon}
        alt="moon"
        width="16"
        height="16"
        role="presentation"
        style={{ pointerEvents: "none" }}
      />
    ),
    unchecked: (
      <img
        src={sun}
        alt="sun"
        width="16"
        height="16"
        role="presentation"
        style={{ pointerEvents: "none" }}
      />
    ),
  }}
  checked={theme === "dark"}
  onChange={e =>
    window.__setPreferredTheme(e.target.checked ? "dark" : "light")
  }
/>

We’ll then use react-hooks to save and update the state.

const [theme, setTheme] = useState(null);

useEffect(() => {
  setTheme(window.__theme);
  window.__onThemeChange = () => {
    setTheme(window.__theme);
  };
}, []);

Okay, so a lot going on here. Let me try to explain these step by step. Firstly, note that the useEffect hook is passed [] as second argument and therefore it’ll be called only once after the first render of the component i.e when the component mounts. So, after the very first render we add the current theme to the state using the setTheme function. We’ve also defined the __onThemeChange method inside of the hook, but I’ll come to it in a while.

Finally, once we have our toggle switch in place, anytime someone toggles it, the callback associated with the onchange is called. This callback in turn calls the __setPreferredTheme method, which - if you remember, we had already declared in the very first step while injecting our javascript (Feel free to scroll up and find the method). __setPrefferedTheme calls setTheme function which, as I already mentioned adds appropriate class ('dark' or 'light') to the body and in addition also calls the __onThemeChange, which like I metioned is defined inside of the useEffect hook in the component and does a simple job of updating the state everytime we toggle the switch between dark and light.

And that’s pretty much it! We should now have a working dark-light mode toggle added to our website. Ofcourse you could play around and tweak as per your need but I guess this much should be good to give you a sense to get started!

I hope you enjoyed this short post and that it helps you in some way or the other. Do let me know your thoughts in the comments.

And as always - Thanks for reading!

References

  1. Gatsby’s dark mode plugin
  2. Have a look at Dan Abramov’s open source Blog
  3. Checkout this blog for a slightly different and more cooler implementation
  4. Clone the awesome react-toggle library

Personal blog by Shubham Singh. I Observe, think and then write.