Dark theme with Stitches

Integrate dark theme support with Stitches on Gatsby.

After tinkering a bit with Stitches, I am very excited to see the improvements in DX, variant-first support, and performance in the CSS-in-JS space! Definitely give their documentation a peruse to see all the brilliance being done.

While exploring this library, I found a small theming gotcha when integrating it with my Gatsby static site. If you are curious about using Stitches with Gatsby supporting a dark theme, read on to see my current solution.

Switching to Stitches

As an experiment, I decided to swap out

styled-component
with
@stitches/react
to test out Stitches' developer experience and performance. I was also curious about the effort of migration and what was required to support my site's dark theme.

Swapping components was very straightforward, especially with the help of the CSS-in-JS VS Code plugin. My initial reason for authoring my components using the tagged template literal method was to maintain closer compatibility with CSS. However, object styles are well supported the JS community and a versatile structure for storing styles.

A small plus, object styles are also a tad faster than runtime-parsed template strings. They also work better with libraries that support a typed interface (ex: vanilla-extract, Stitches).

Theming with Stitches in Gatsby

Back to the theming problem! The only other migration step was to update the theming layer. I followed the dark theme pattern well explained and shared from Josh Comaeu – thanks Josh! Using the Stitches

helper, I simplifed my code a good bit. Stitches handles the addition of CSS custom properties based on your theme configuration, allowing me to remove the chunk of code that was manually handling this step.

Here is the final required code used in the site's SRR step:

// gatsby-ssr.js

import React from 'react';
import Terser from 'terser';

import { COLOR_MODE_KEY } from '../constants';
import { darkTheme, getCssString } from '../stitches.config';

function setColorsByTheme() {
  const colorModeKey = 'πŸ”‘';
  const darkThemeClassName = 'πŸŒ™';

  const mql = window.matchMedia('(prefers-color-scheme: dark)');
  const prefersDarkFromMQ = mql.matches;
  const persistedPreference = localStorage.getItem(colorModeKey);

  let colorMode = 'light';

  const hasUsedToggle = typeof persistedPreference === 'string';

  if (hasUsedToggle) {
    colorMode = persistedPreference;
  } else {
    colorMode = prefersDarkFromMQ ? 'dark' : 'light';
  }

  let root = document.documentElement;
  if (colorMode === 'dark') {
    root.classList.add(darkThemeClassName);
  }
}

const MagicScriptTag = () => {
  const boundFn = String(setColorsByTheme)
    .replace('πŸ”‘', COLOR_MODE_KEY)
    .replace('πŸŒ™', darkTheme.className);

  let calledFunction = `(${boundFn})()`;
  calledFunction = Terser.minify(calledFunction).code;

  return <script dangerouslySetInnerHTML={{ __html: calledFunction }} />;
};

const FallbackStyles = () => {
  return (
    <style
      id="stitches"
      dangerouslySetInnerHTML={{
        __html: getCssString(),
      }}
    />
  );
};

export const onRenderBody = ({ setPreBodyComponents, setHeadComponents }) => {
  setHeadComponents(<FallbackStyles key="fallback-styles" />);
  setPreBodyComponents(<MagicScriptTag key="magic-script-tag" />);
};

This is a slight variation of Josh's strategy and solution outlined in the Stitches tutorial. For some extra reading, I encourage checking out Josh's full dark theme solution or digging into the source code example!

Final adjustments

The only other modification is updating the

ThemeContext
to toggle the appropriate className on the root document element:

// ThemeContext.tsx

import React from 'react';
import { COLOR_MODE_KEY } from '../constants';
import { darkTheme } from '../stitches.config';

type ColorMode = 'light' | 'dark';
type ThemeContextValue = {
  colorMode: ColorMode;
  setColorMode: (newValue: ColorMode) => void;
};

const themes = {
  light: 'light',
  dark: darkTheme.className,
};

export const ThemeContext = React.createContext<ThemeContextValue>({
  colorMode: 'light',
  setColorMode: () => {},
});

export const ThemeProvider: React.FC = ({ children }) => {
  const [colorMode, rawSetColorMode] = React.useState<ColorMode>('light');

  React.useEffect(() => {
    const root = document.documentElement;
    const initialColorMode = root.classList.contains(themes['dark'])
      ? 'dark'
      : 'light';
    rawSetColorMode(initialColorMode);
  }, []);

  const contextValue: ThemeContextValue = React.useMemo(() => {
    function setColorMode(newValue: ColorMode): void {
      document.documentElement.classList.remove(themes[colorMode]);
      document.documentElement.classList.add(themes[newValue]);
      localStorage.setItem(COLOR_MODE_KEY, newValue);
      rawSetColorMode(newValue);
    }

    return {
      colorMode,
      setColorMode,
    };
  }, [colorMode, rawSetColorMode]);

  return (
    <ThemeContext.Provider value={contextValue}>
      {children}
    </ThemeContext.Provider>
  );
};

When running in development, the site may initially load in the default light mode. The page adds the dark theme CSS custom properties when using

darkTheme.className
in
ThemeContext.tsx
. I'm not a Stitches pro, but looking at the current source, it appears this occurs in the
className
getter function
which triggers a stylesheet update. Note that this may change in the future, as Stitches is in beta.

When built for production, Gatsby statically generates each page which all include the Theme Context. The Theme Context adds the dark theme variables to the site when run and rendered as part of the static build. This avoids the flash of the light theme on the initial page load.

Conclusion

This was a pretty niche problem. I thought I'd share my current solution since I hadn't discovered one myself in the docs or example repos. Happy styling with Stitches, and a big thanks to the team's awesome work. Looking forward to seeing Stitches evolve even more. 🧢πŸͺ‘

Subscribe to the newsletter

Be the first to know when I post something new! Thoughts about code, design, startups and other interesting things.

Β© 2025 Sam Rose
All Rights Reserved.