Gatsby Dark Mode with Themes

October 21, 2024

Implementing Dark Mode in GatsbyJS 4.7

With the release of GatsbyJS 4.7, developers now have more tools and features at their disposal to create dynamic and responsive websites. One popular feature that can significantly enhance user experience is the ability to toggle between light and dark modes. This feature not only adds aesthetic appeal but also helps reduce eye strain and conserve battery power. In this blog post, we will explore how to implement a dark mode in GatsbyJS 4.7, using React context, local storage, and CSS transitions.

Setup

We will start by defining the context for our dark mode. In the RootLayout.js file, we add a new context provider DarkModeContextProvider. This provider is used to pass down the dark mode state and the function to toggle it to other components in the application.

import React from 'react';
import { DarkModeContextProvider } from './context/DarkModeContext';

export default function RootLayout({ children }) {
  return (
    <DarkModeContextProvider>
      {children}
    </DarkModeContextProvider>
  );
}

Themes Definition

We define two theme styles for light and dark modes in theme.js. Each theme style includes the text and background colors, among others, as shown below:

import * as colors from './colors';

export const themeLight = {
  theme: 'light',
  text: colors.textDark,
  background: '#fff',
  cardBackground: '#0000001a',
  shadow: '#0000001a',
};

export const themeDark = {
  theme: 'dark',
  text: '#fff',
  background: colors.dark,
  cardBackground: '#ffffff2f',
  shadow: '#FFC836',
};

Creating the Context for Dark Mode

We create a new context DarkModeContext in DarkModeContext.js and use a state variable isDark to keep track of whether the dark mode is enabled. The setDarkMode function changes the isDark state and stores the user’s preference in localStorage.

import React, { createContext, useState, useEffect } from 'react';
import { ThemeProvider } from '@emotion/react';
import { themeDark, themeLight } from '../styles/theme';

export const DarkModeContext = createContext();

export function DarkModeContextProvider({ children }) {
  const [isDark, setIsDark] = useState(false);

  const setDarkMode = (darkness) => {
    setIsDark(darkness);
    localStorage.setItem('dark-mode', darkness ? 'dark' : 'light');
  };
//...
}

Toggling Between Light and Dark Modes

In the NightDayToggle.js file, we create a component that toggles between light and dark modes. The checked prop determines whether the toggle is in the on (dark mode) or off (light mode) state. The changed prop is a function that handles the user’s interaction with the toggle.

import React from 'react';

import './styles.css';

export const NightDayToggle = ({ checked, changed }) => {
  return (
    <div className='toggle'>
      <label className='visually-hidden' htmlFor='dark-toggle'>
        Toggle between dark and light mode
      </label>
      <input
        id='dark-toggle'
        className='toggle-input'
        type='checkbox'
        checked={checked}
        onChange={changed}
      />
      <div className='toggle-bg'></div>
      <div className='toggle-switch'>
        <div className='toggle-switch-figure'></div>
        <div className='toggle-switch-figureAlt'></div>
      </div>
    </div>
  );
};

Styling the Toggle

Finally, we style the toggle by creating a style.css file. The CSS in this file is responsible for the appearance of the toggle button and the transition between light and dark modes.

/* Credit to Jason Tyler */

.toggle,
.toggle * {
  box-sizing: content-box;
}

.toggle {
  position: absolute;
  display: inline-block;
  width: 100px;
  padding: 4px;
  border-radius: 40px;
  transform: scale(0.4);
  flex-shrink: 0;
  margin-left: -20px;
  margin-right: -30px;
  top: 75px;
  right: 10px;
}

.toggle-bg {
  position: absolute;
  top: -4px;
  left: -4px;
  width: 100%;
  height: 100%;
  background-color: #c0e6f6;
  border-radius: 40px;
  border: 4px solid #81c0d5;
  transition: all 0.1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.toggle-input {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border: 1px solid red;
  border-radius: 40px;
  z-index: 2;
  opacity: 0;
}

.toggle-switch {
  position: relative;
  width: 40px;
  height: 40px;
  margin-left: 50px;
  background-color: #f5eb42;
  border: 4px solid #e4c74d;
  border-radius: 50%;
  transition: all 0.1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

.toggle-switch-figure {
  position: absolute;
  bottom: -14px;
  left: -50px;
  display: block;
  width: 80px;
  height: 30px;
  border: 8px solid #d4d4d2;
  border-radius: 20px;
  background-color: #fff;
  transform: scale(0.4);
  transition: all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.toggle-switch-figure:after {
  content: '';
  display: block;
  position: relative;
  top: -65px;
  right: -42px;
  width: 15px;
  height: 15px;
  border: 8px solid #d4d4d2;
  border-radius: 100%;
  border-right-color: transparent;
  border-bottom-color: transparent;
  transform: rotateZ(70deg);
  background-color: #fff;
}
.toggle-switch-figure:before {
  content: '';
  display: block;
  position: relative;
  top: -25px;
  right: -10px;
  width: 30px;
  height: 30px;
  border: 8px solid #d4d4d2;
  border-radius: 100%;
  border-right-color: transparent;
  border-bottom-color: transparent;
  transform: rotateZ(30deg);
  background-color: #fff;
}

.toggle-switch-figureAlt {
  content: '';
  position: absolute;
  top: 5px;
  left: 2px;
  width: 2px;
  height: 2px;
  background-color: #efeeda;
  border-radius: 100%;
  border: 4px solid #dee1c5;
  box-shadow: 42px -7px 0 -3px #fcfcfc, 75px -10px 0 -3px #fcfcfc,
    54px 4px 0 -4px #fcfcfc, 83px 7px 0 -2px #fcfcfc, 63px 18px 0 -4px #fcfcfc,
    44px 28px 0 -2px #fcfcfc, 78px 23px 0 -3px #fcfcfc;
  transition: all 0.12s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  transform: scale(0);
}

.toggle-switch-figureAlt:before {
  content: '';
  position: absolute;
  top: -6px;
  left: 18px;
  width: 7px;
  height: 7px;
  background-color: #efeeda;
  border-radius: 100%;
  border: 4px solid #dee1c5;
}

.toggle-switch-figureAlt:after {
  content: '';
  position: absolute;
  top: 19px;
  left: 15px;
  width: 2px;
  height: 2px;
  background-color: #efeeda;
  border-radius: 100%;
  border: 4px solid #dee1c5;
}

.toggle-input:checked ~ .toggle-switch {
  margin-left: 0;
  border-color: #dee1c5;
  background-color: #fffdf2;
}

.toggle-input:checked ~ .toggle-bg {
  background-color: #484848;
  border-color: #202020;
}

.toggle-input:checked ~ .toggle-switch .toggle-switch-figure {
  margin-left: 40px;
  opacity: 0;
  transform: scale(0.1);
}

.toggle-input:checked ~ .toggle-switch .toggle-switch-figureAlt {
  transform: scale(1);
}

Conclusion

With these steps, we have successfully added a functional dark mode to our GatsbyJS 4.7 application. This not only makes the application more user-friendly but also adds a modern touch to its design. The implementation of the dark mode feature showcases how GatsbyJS 4.7, coupled with React context and local storage, can be used to enhance the user interface and experience of an application.


Written by Matt Gregg, a UI engineer who lives and works in Minneapolis, MN

Have something to say about this post? Tweet at me