loading...
Cover image for 🌙 How I set Dark Mode for Gatsby website

🌙 How I set Dark Mode for Gatsby website

amaltapalov profile image Amal Tapalov ・4 min read

I recently decided to add dark and light mode to my website so that website visitors can easily switch to an eye-friendly design whenever they want.

Why dark mode?

Dark and light mode can provide user-friendly experience on website. I choose to implement toggleable dark mode (reference to neon 80's theme) and light mode (classic style wtih accent colors) and , in the same time, it adds a some level interaction to my website.

What I used?

I found out there is a special plugin in Gatsby plugin library gatsby-plugin-dark-mode but I decided not to touch ready-to-use solution but to dive deep to custom one.

In order to implement dark-light mode I chose to stay with SSR and React Hooks as useEffect and useState.

Implementation

  1. First of all I decided to add theme item and its value to localStorage. I used gatsby-ssr.js to set preBodyComponent in order to have script uploaded as soon as possible.
const React = require('react')

exports.onRenderBody = ({ setPreBodyComponents }) => {
  setPreBodyComponents([
    React.createElement('script', {
      dangerouslySetInnerHTML: {
        __html: `
          (() => {    
            window.__onThemeChange = function() {};                
            function setTheme(newTheme) {                  
              window.__theme = newTheme;                  
              preferredTheme = newTheme;                  
              document.body.className = newTheme;                 
              window.__onThemeChange(newTheme);                
            }

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

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

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

            setTheme(preferredTheme || (darkQuery.matches ? 'light' : 'dark'))
          })()
        `,
      },
    }),
  ])
}
  1. After that I went to Header component and added our useEffect and useState hooks.

What does useEffect do?

By using this Hook, you tell React that your component needs to do something after render. React will remember the function you passed (we’ll refer to it as our “effect”), and call it later after performing the DOM updates.

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

Then I needed to add useState hook to trigger state change every time I want to switch theme.

There is a big BUT here. I faced up to using null in useState hook that caused rendering Header twice every time clicking on theme toggler. The solution is to provide an initial state to prevent double render.

Here will be a screenshot

const [theme, setTheme] = useState(websiteTheme)

You can see that initial state of useState hook is websiteTheme. It holds a window.__theme value you can see in gatsby-ssr.js. And I added a condition for server side rendering because THERE IS NO WINDOW while Gatsby is building website.

Kyle Mathews states:

During development, react components are only run in the browser where window is defined. When building, Gatsby renders these components on the server where window is not defined.

let websiteTheme
if (typeof window !== `undefined`) {
  websiteTheme = window.__theme
}

In the end I added a ThemeToggle function which toggles website theme between dark and light mode

const ThemeToggle = () => {
  window.__setPreferredTheme(websiteTheme === 'dark' ? 'light' : 'dark')
}

and toggle button

<button onClick="{ThemeToggle}">
  {theme === 'dark' ? (
  <img src="{sun}" alt="Light mode" />
  ) : (
  <img src="{moon}" alt="Dark mode" />
  )}
</button>

Here is complete version of Header component:

// src/components/Header.index.js

import React, { useState, useEffect } from 'react'
import sun from '../../images/sun.svg'
import moon from '../../images/moon.svg'

const Header = props => {
  let websiteTheme
  if (typeof window !== `undefined`) {
    websiteTheme = window.__theme
  }

  const [theme, setTheme] = useState(websiteTheme)

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

  const ThemeToggle = () => {
    window.__setPreferredTheme(websiteTheme === 'dark' ? 'light' : 'dark')
  }

  return (
    ...skipped...
        <button onClick={ThemeToggle}>
          {theme === 'dark' ? (
            <img src={sun} alt="Light mode" />
          ) : (
            <img src={moon} alt="Dark mode" />
          )}
        </button>
    ...skipped...
  )
}

export default Header

So we are almost done. The last thing we need to add is out styles for dark and light theme. I used GlobalStyle providing by styled-components. Don't worry I will provide solution with css as well. So, we need to create a GlobalStyle.js component in style folder. Inside GlobalStyle.js file we type this:

// src/styles/GlobalStyle.js

import { createGlobalStyle } from 'styled-components'
export const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    background-color: var(--bg);
    color: var(--textNormal);

    &.dark {
      --bg: #221133;
      --textNormal: #fff;
    }

    &.light {
      --bg: #fff;
      --textNormal: #000;
    }
  `

After I go to Layout.js component which is responsible for website layout and insert GlobalStyle into it.

// src/layout/index.js

...skiped...
import { ThemeProvider } from 'styled-components'
import { GlobalStyle } from '../styles/GlobalStyle'

export default ({ children }) => {
  return (
    <ThemeProvider theme={styledTheme}>
      <GlobalStyle />
        <Header />
        {children}
        <Footer />
    </ThemeProvider>
  )
}

That's it! Every time you click on toggle button you will change theme between dark and light versions.

Thanks for reading and happy coding 😉 !

Useful links:

Discussion

pic
Editor guide
Collapse
deathshadow60 profile image
deathshadow60

Sheesh... talk about convoluted and a NASTY case of "JS for nothing"

<body>
    <input type="checkbox" id="toggle_dayNight" hidden>
    <div class="dayNight">

Set the DIV up to behave as a body replacement (I often have this in place anyways as a fix for the "double scroll bar" modal issue)...

Then just "#toggle_dayNight:checked + .dayNight {}" to set the override colours.

Not a single line of scripting needed. "hidden" is your new best friend.

JS for nothin' and your scripts for free. That ain't workin', that's not how you do it...

Collapse
amaltapalov profile image
Amal Tapalov Author

Some devs prefered write this way, some people use checked property. There is always a place to play with code. Everybody chooses what they prefer. Piece!

Example code:

<input
    id="theme-mode"
    type="checkbox"
    checked={theme === 'dark'}
    onChange={e => {
    window.__setPreferredTheme(e.target.checked ? 'dark' : 'light')
   }}
/>        
Collapse
deathshadow60 profile image
deathshadow60

Again, not JavaScript's job if you can avoid it, since you're alienating large swaths of users. Much less onEvent attributes being relics of two decades ago that should have been axed at that time...

... and using some rubbish framework or pre-processor to do "templates" with it doesn't fix that. If anything, it simply proves that people are throwing JavaScript client-side in ways that are NONE of JavaScript's flipping business.

See 90%+ of what people do with mentally enfeebled trash like React or jQuery... the stuff I have to rip out in my day job of sites in court for WCAG violations!

Thread Thread
amaltapalov profile image
Amal Tapalov Author

So much pain is in the comment above 😀
Thanks for WCAG reference

Collapse
skazko profile image
skazko

Thanks for great post.
It seems to me there's a typo, and need to swap modes:

...
darkQuery.addListener(e => {
  window.__setPreferredTheme(e.matches ? 'dark' : 'light')
})

setTheme(preferredTheme || (darkQuery.matches ? 'dark' : 'light'))

Also check your website too.

Collapse
alexr89 profile image
Alex

Hey,

Sorry for being quite vague but do you know how to get something like this working using next?

I have it all set up and working, however there is a flicker if you select a dark theme and then refresh the page. The reason for this is because the _app.js initial state is "light", meaning that it sets the light theme, then checks the local storage - finds the theme entry - and sets the theme dark.

So essentially afaik it goes: renders on server -> sends over light theme -> checks local storage and finds dark theme -> renders dark theme

I don't know how to get around this issue, other than blocking the entire render of the app while we check local storage for a theme?

Collapse
lucascoorek profile image
Łukasz Kurek

Hi Amal,
I don't understand how this window.__onThemeChange function works.

First this function is declared: window.__onThemeChange = function() {};

And them it is called from inside of setTheme function:
function setTheme(newTheme) {
window.theme = newTheme;
window.
onThemeChange(newTheme);
}
Then you use it in Component in useEffect by adding into body of this onThemeChange function change to Components state:

useEffect(() => {
toggleTheme(websiteTheme);
window.onThemeChange = () => {
toggleTheme(window.
theme);
};
}, []);

How is it that you call window.__onThemeChange(newTheme) from within setTheme() when it is only declared as a empty function at the top of this file?
Can you explain how it works?

Thank you in advance.

Collapse
devcoder profile image
devcoder

why not just use the plugin rather than re-inventing the wheel, not trying to come across negative either