Using Vanilla Extract with next-themes

Using Vanilla Extract with next-themes

Vanilla extract has first class support for different themes, and gives you complete control over how to handle them in your app. In this post I’ll show you how to set up theming in vanilla extract and how to handle them in Next.js apps using next-themes.

Setting up themes in Vanilla Extract

This guide assumes you’ve installed vanilla-extract according to the docs. Start by creating a file vars.css.ts in the /styles folder that will contain all of the theme setup.

import { createGlobalTheme, createTheme, createThemeContract } from "@vanilla-extract/css";

const global = createGlobalTheme("html", {
  space: {
    none: 0,
    small: '4px',
    medium: '8px',
    large: '12px'
  },
  fonts: {
    heading: "Inter, sans-serif",
    body: "system-ui" 	
  }
});

const colors = createThemeContract({
  background: null,
  text: null
});

export const lightTheme = createTheme(colors, {
  background: "white",
  text: "black"
});

export const darkTheme = createTheme(colors, {
  background: "black",
  text: "white"
});

export const vars = { ...global, colors };

First we create a global theme containing all the theme variables that don’t change between themes, like spacing, fonts & line-height.

Then we create a “theme contact”, basically a blueprint for how the theme-specific values will look like and pass it as the first argument to createTheme when defining our light and dark versions.

Finally export the combined theme for use inside you css.

Note that the reason we did not put everything in the theme contract is we would then have had to redefine those values for every theme. The great article Theming a React Application with Vanilla Extract goes deeper into this and setting up your theme.

Applying Vanilla Extract Themes with next-themes

Vanilla Extract isn’t concerned with how the themes are applied. Doing this is surprisingly tricky and the main reason I am using existing solutions like next-themes.

Install the package and start by adding the theme provider to your custom App.

yarn add next-themes
import type { AppProps } from "next/app";
import { ThemeProvider } from 'next-themes'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

export default MyApp

Vanilla Extract gives us a className for each theme, so we need to change the theme provider from the default attribute [data-theme] to class

import type { AppProps } from "next/app";
import { ThemeProvider } from 'next-themes'

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider attribute="class">
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

export default MyApp

The final step is specifying the classNames to apply for each theme. We can do this by adding the value prop with a map of themes and their respective classes.

Import the classes from your theme like below:

import type { AppProps } from "next/app";
import { ThemeProvider, useTheme } from "next-themes";
import { lightTheme, darkTheme } from "../styles/vars.css";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider
      attribute="class"
      value={{
        light: lightTheme,
        dark: darkTheme,
      }}
    >
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;

That’s it for setup! By default next-themes will default to matching the theme set in your system.

Using the theme

Import your theme variables into any css.ts file, the easiest setup for this case would be:

import { globalStyle } from "@vanilla-extract/css";
import { vars } from "./vars.css";

globalStyle("body", {
  background: vars.colors.background,
  color: vars.colors.text,
});

Changing themes

There is nothing specific about vanilla-extract when changing themes, simply follow the guidelines in the next-themes docs. An example component might look like this:

import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

const ThemeChanger = () => {
  const [mounted, setMounted] = useState(false);
  const { theme, setTheme } = useTheme();

  // When mounted on client, now we can show the UI
  useEffect(() => setMounted(true), []);

  if (!mounted) return null;

  return (
    <div>
      Theme:
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="system">System</option>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
      </select>
    </div>
  );
};

export default ThemeChanger;

Bonus: Adding more themes

There is nothing stopping you from adding more than dark/light themes, let’s have some fun by adding a Christmas theme:


export const darkTheme = createTheme(colors, {
  background: "black",
  text: "white"
});

export const christmasTheme = createTheme(colors, {
  background: "red",
  text: "green"
});

export const vars = { ...global, colors };
import type { AppProps } from "next/app";
import { ThemeProvider, useTheme } from "next-themes";
import { lightTheme, darkTheme, christmasTheme } from "../styles/vars.css";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider
      attribute="class"
      value={{
        light: lightTheme,
        dark: darkTheme,
        christmas: christmasTheme
      }}
    >
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

const ThemeChanger = () => {
  const [mounted, setMounted] = useState(false);
  const { theme, setTheme } = useTheme();

  // When mounted on client, now we can show the UI
  useEffect(() => setMounted(true), []);

  if (!mounted) return null;

  return (
    <div>
      Theme:
      <select value={theme} onChange={(e) => setTheme(e.target.value)}>
        <option value="system">System</option>
        <option value="light">Light</option>
        <option value="dark">Dark</option>
        <option value="christmas">Christmas</option>
      </select>
    </div>
  );
};

export default ThemeChanger;

Summary

Vanilla Extract is a really powerful way of working with CSS, and I am very excited for its future. It feels like the natural successor of CSS modules, with a much improved developer experience thanks to being in typescript. I also love how we can still use it with existing projects, like next-themes.

If you enjoyed this post, press the heart button below 👇

Related Posts