Back to blog

Design tokens with Tailwind CSS v4

January 5, 20252 min read
TailwindDesign systemCSS

The problem it solves

Before Tailwind v4, the color palette lived in `tailwind.config.js` as a JS object (`theme.extend.colors`). It worked, but whenever you needed to manage a dark mode with different shades or reuse a color outside of Tailwind (in custom CSS, a canvas, a third-party component), the same value ended up duplicated in multiple places.

What changes with `@theme`

Tailwind v4 moves the configuration from JS to CSS. Tokens become real custom properties, declared directly in the stylesheet:

@theme inline {
  --color-primary: oklch(0.55 0.18 250);
  --color-primary-foreground: oklch(0.98 0 0);
  --radius-md: 0.5rem;
}

The benefit is that `--color-primary` becomes a true CSS variable accessible everywhere: in a Tailwind class (`bg-primary`), in handwritten CSS, or even in JS via `getComputedStyle`. A single source of truth instead of three.

The real advantage: dark mode

This is where it gets interesting for managing light/dark modes cleanly. Instead of duplicating every class with a `dark:` prefix, you redefine the same variables inside a `.dark` block:

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
}

.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
}

A component using `bg-background text-foreground` needs no conditional logic: it follows the variable, and the variable changes based on the class applied to `<html>`. On a project with many components, this avoids writing `dark:` on every colored element.

A pitfall to watch out for

A poorly named token or one with an overly saturated chroma value (in OKLCH, the hue can quickly become garish) propagates instantly wherever it’s used—unlike an isolated Tailwind class that you can fix in one place. It’s better to test values in both light and dark modes before committing them, rather than discovering they’re broken three screens later.

Design tokens with Tailwind CSS v4 | Izayid Ali