Adding dark mode to website

November 04, 2019

Time to read: 4 min

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: