Dark Mode with Design Tokens in Tailwind CSS

An approach for implementing design system tokens in Tailwind, for more symbolic theming
3 min read

This is something I’ve been using in projects I work on, while using Tailwind CSS.

With this approach, we define all our design tokens once as variables and map them to their underlying colors. We can use media queries (e.g. for dark mode), to vary the values of these variables.

This allows us to build dark-mode supporting sites without requiring the overhead of applying tons of dark: styles across the whole codebase. As a side benefit, we can also make global theme updates by modifying our design tokens, not every single component that uses the colors.

Before we start

There’s three separate pieces to this approach:

  1. Extract the color scales from Tailwind to CSS Variables with a plugin.
  2. Implement design tokens as their own variables in CSS, that reference the original Tailwind colors.
  3. Extend Tailwind’s theme to add new “colors” for each of these design tokens which reference our variables.

Exporting Colors as Variables

First, we need to advertise the Tailwind color scales as CSS variables. This GIST by @Merott on Github contains a plugin that accomplishes this, (reproduced below), with some small modifications to keep TypeScript happy. This exposes colors to variables such as --color-blue-500:

  plugins: [
    // https://gist.github.com/Merott/d2a19b32db07565e94f10d13d11a8574
    // add to your tailwind CSS plugins
    function({ addBase, theme }: any) {
      function extractColorVars(colorObj: any, colorGroup = '') {
        return Object.keys(colorObj).reduce((vars: any, colorKey: any) => {
          const value = colorObj[colorKey];

          const newVars : any =
            typeof value === 'string'
              ? { [`--color${colorGroup}-${colorKey}`]: value }
              : extractColorVars(value, `-${colorKey}`);

          return { ...vars, ...newVars };
        }, {});
      }

      addBase({
        ':root': extractColorVars(theme('colors')),
      });
    },
  ]

Creating Design Tokens

Add declarations for all your tokens as normal CSS variables in your main CSS file. (I use the file containing the @tailwind declarations).

One could imagine other ways to use this to enable multiple themes besides dark/light using classes instead of prefers-color-scheme.

:root {
  --background: var(--color-neutral-100);
  --text-primary: var(--color-neutral-950);
  --primary: var(--color-blue-500);
  --surface: var(--color-neutral-100);
  --surface-hover: var(--color-neutral-200);
  /* .. and so on */
}

/* your dark theme */
@media (prefers-color-scheme: dark) {
  :root {
    --background: var(--color-neutral-950);
    --text-primary: var(--color-neutral-100);
    --primary: var(--color-blue-500);
    --surface: var(--color-neutral-900);
    --surface-hover: var(--color-neutral-800);
    /* .. and so on *//
  }
}

Adding as Colors

Next, add these as actual colors in your Tailwind config file, within the theme colors section. This will expose the colors as named colors you can use within your app.

theme: {
  extend: {
    colors: {
      'background': "var(--background)",
      'text-primary': "var(--text-primary)",
      'primary': "var(--primary)",
      'surface': "var(--surface)",
      'surface-hover': "var(--surface-hover)",
    }
  }
}

Using in your components

Now, instead of writing something like: bg-neutral-100 dark:bg-neutral-950, you can simply write something like: bg-background, which will work for both light/dark mode. Additionally, if you decide to change it, you can easily do so.

And - if you decide to change your color scheme down the line, all that’s needed is to update the definition of the token in your main CSS file to its new value.

<!-- this -->
<div class="bg-neutral-100 dark:bg-neutral-950"><!--... ---></div>

<!-- becomes -->
<div class="bg-background"><!--... ---></div>

Why Tailwind?

I used to be against things like Tailwind, preferring manually crafted CSS. However, I’ve found it to be very useful especially when dealing with components, and it makes it very easy to reason about how a component is styled.

When opening a component’s markup in something like React to make changes, all the information you need to understand it is contained within. There are no separate styles applied globally that might conflict with things and changes to this component don’t also unintentionally impact other areas.

Subscribe to my Newsletter

Like this post? Subscribe to get notified for future posts like this.

Change Log

  • 10/21/2024 - Initial Revision

Found a typo or technical problem? file an issue!