Skip to content

Instantly share code, notes, and snippets.

@kaicataldo
Last active April 13, 2021 19:13
Show Gist options
  • Save kaicataldo/f28b6adf941d1575afa78e647624a327 to your computer and use it in GitHub Desktop.
Save kaicataldo/f28b6adf941d1575afa78e647624a327 to your computer and use it in GitHub Desktop.
Dark mode using Gatsby.js
import React from 'react';
export const onRenderBody = function({ setPreBodyComponents }) {
// Load dark mode script in head to prevent FOUC.
setPreBodyComponents([
<script
type="text/javascript"
key="theme-initializer"
src="/set-dark-mode.js"
/>,
]);
};
'use strict';
/*
* This script isn't bundled by Gatsby and is instead a static asset that is loaded into the head. This allows it to be
* cached on future loads. It intentionally is blocking to ensure that there isn't a FOUC when the rest of the
* bundle loads and is hydrated.
*/
(function() {
var mode, colorScheme;
try {
mode = window.localStorage.getItem('color-scheme-mode') || 'auto';
} catch (err) {}
colorScheme =
!mode || mode === 'auto'
? window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
: mode;
document.body.setAttribute('data-color-mode', mode);
document.body.setAttribute('data-color-scheme', colorScheme);
})();
import { useState, useEffect } from 'react';
export const AUTO_MODE = 'auto';
export const LIGHT_MODE = 'light';
export const DARK_MODE = 'dark';
export const MODES = [AUTO_MODE, LIGHT_MODE, DARK_MODE];
const MATCH_MEDIA_QUERY = `(prefers-color-scheme: ${DARK_MODE})`;
function isBrowser() {
return typeof window !== 'undefined';
}
function getInitialMode() {
let mode;
try {
mode = window.localStorage.getItem('color-scheme-mode');
} catch {}
return mode || AUTO_MODE;
}
function getColorScheme(mode) {
return mode === AUTO_MODE
? isBrowser() && window.matchMedia(MATCH_MEDIA_QUERY).matches
? DARK_MODE
: LIGHT_MODE
: mode;
}
export default function useDarkMode() {
const [mode, setMode] = useState(getInitialMode());
const [colorScheme, setColorScheme] = useState(getColorScheme(mode));
function cycleMode() {
setMode(MODES[(MODES.indexOf(mode) + 1) % 3]);
}
useEffect(() => {
const darkColorSchemeQueryHandler = e => {
let colorScheme;
try {
colorScheme = window.localStorage.getItem('color-scheme-mode');
} catch {}
if (colorScheme && colorScheme !== AUTO_MODE) {
return;
}
setColorScheme(e.matches ? DARK_MODE : LIGHT_MODE);
};
const darkColorSchemeQuery = window.matchMedia(MATCH_MEDIA_QUERY);
darkColorSchemeQuery.addListener(darkColorSchemeQueryHandler);
return () =>
darkColorSchemeQuery.removeListener(darkColorSchemeQueryHandler);
}, []);
useEffect(() => {
try {
window.localStorage.setItem('color-scheme-mode', mode);
} catch (err) {}
document.body.setAttribute('data-color-mode', mode);
setColorScheme(getColorScheme(mode));
}, [mode]);
useEffect(() => {
document.body.setAttribute('data-color-scheme', colorScheme);
}, [colorScheme]);
return [mode, cycleMode];
}
@kaicataldo
Copy link
Author

kaicataldo commented Feb 23, 2020

This is my solution to prevent a FOUC (flash of unstyled content) when implementing a dark mode toggle on my Gatsby site (https://kaicataldo.com).

The set-dark-mode.js script referenced in gatsby-ssr.js gets loaded in the head as a blocking static asset so that it executes before the body is parsed and rendered. I used to inject it directly into the markup, but have since externalized it to allow for the script to be cached on subsequent loads. I'm using CSS variables to change the colors in the styles.

Example of what the styles look like:

:root {
  /* Light theme */
  --text-color: #333333;
  --background-color: #ffffff;
}

/* Dark theme */
[data-color-scheme='dark'] {
  --text-color: #ffffff;
  --background-color: #181b25;
}

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

The hook is used in my main Layout component like so:

 const [mode, cycleMode] = useDarkMode();

These are both passed to child components so that they can display the correct icon using mode as well as cycling the mode (between auto, light, and dark) using the cycleMode callback.

Would love to hear about other possible solutions!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment