Dark theme with Stitches and Next.js
Rude Ayelo
/
March 2021
October 2021 update: Updated the code examples to match the Stitches v1 API.
It seems like you can't make a website nowadays without pushing in dark mode. Let's see how it's done using two of my most favourite pieces of software ever: Next.js and Stitches:
First of all, you'll need to setup Stitches and your dark theme:
// stitches.config.ts
import { createCss } from "@stitches/react";
export const stitchesConfig = createCss({
  theme: {
    colors: {
      gray900: "hsl(205,5%,7%)",
      gray700: "hsl(205,5%,25%)",
      gray500: "hsl(205,5%,35%)",
      gray50: "hsl(205,5%,95%)",
      blue500: "hsl(205,90%,45%)",
      // Alias
      primary: "$gray900",
      secondary: "$gray700",
      tertiary: "$gray500",
      link: "$blue500",
      background: "$gray50",
      border: "$gray900",
    },
    // ...
  },
  media: {
    dark: "(prefers-color-scheme: dark)",
  },
});
export const darkTheme = stitchesConfig.createTheme({
  colors: {
    primary: "$gray100",
    secondary: "$gray200",
    tertiary: "$gray300",
    link: "$blue500",
    background: "$gray900",
    border: "$gray100",
  },
});
Stitches relies on CSS custom properties (also known as variables) for the theme tokens so by creating new themes you're basically creating a new set of custom properties attached to a class name, how to apply that class name is up to you.
The simplest solution would be adding a button somewhere in the page with an
onClick event that would toggle darkTheme.className on and off whenever it's
clicked. But we want to offer something better in terms of user experience so
we'll use next-themes by Paco Coursey.
To enable your dark theme you'll need to add the ThemeProvider from
next-themes in your
_app.js file with a
little bit of config:
// _app.tsx
import { ThemeProvider } from "next-themes";
import { darkTheme } from "./stitches.config.ts";
function App({ Component, pageProps }) {
  return (
    <ThemeProvider
      attribute="class"
      defaultTheme="system"
      value={{
        dark: darkTheme.className,
        light: "light",
      }}
    >
      <Component {...pageProps} />
    </ThemeProvider>
  );
}
export default App;
- By default 
ThemeProvideradds the current theme name to adata-themeattribute in thehtmltag, we are modifying that behaviour to use theclassattribute instead. - When passing a 
valueall of the default themes (lightanddark) are overriden so we want to make sure we name thelighttheme even if we're not using the class name at all. 
Finally, we want some mechanism to switch between themes. We'll use information that's only available in the client so to avoid an hydration mismatch in the server we'll need to delay rendering the component until it's mounted.
// ./ThemeToggle.tsx
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
export const ThemeToggle = () => {
  const [mounted, setMounted] = useState(false);
  const { setTheme, resolvedTheme } = useTheme();
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  const toggleTheme = () => {
    const targetTheme = resolvedTheme === "light" ? "dark" : "light";
    setTheme(targetTheme);
  };
  return (
    <button className={toggleButton()} onClick={toggleTheme}>
      Switch theme
    </button>
  );
};
And done! You can try it here:
Handling the FODT (Flash Of Default Theme)
We might (should) be generating our static pages at build time, using the above technique alone will cause a flash of light theme for users with dark theme enabled at the system level.
To fix that issue we'll need some global styles to manually override the theme tokens when the user prefers a dark color scheme:
// stitches.config.tsx
export const globalStyles = stitchesConfig.globalCss({
  "@dark": {
    // notice the `media` definition on the stitches.config.ts file
    ":root:not(.light)": {
      ...Object.keys(darkTheme.colors).reduce((varSet, currentColorKey) => {
        const currentColor = darkTheme.colors[currentColorKey];
        const currentColorValue =
          currentColor.value.substring(0, 1) === "$"
            ? `$colors${currentColor.value}`
            : currentColor.value;
        return {
          [currentColor.variable]: currentColorValue,
          ...varSet,
        };
      }, {}),
    },
  },
});
globalStyles();