---
title: "Tailwind v4: Theme and Design Tokens"
slug: "tailwind-theme"
category: "frontend"
tags: ["tailwind", "css", "frontend", "design-tokens", "theming", "custom-properties", "oklch"]
status: "stable"
last_updated: 2026-05-14
summary: "Define design tokens once in @theme, get CSS custom properties and utility classes for free, and support runtime theme switching without a build step."
related: ["[[frontend/tailwind]]", "[[frontend/css]]", "[[frontend/css-custom-properties]]", "[[frontend/tailwind-dark-mode]]", "[[comparisons/tailwind-vs-css-modules]]"]
---

## Overview

`@theme` is the v4 replacement for the JavaScript config object. Every declaration becomes a CSS custom property at `:root` and a generated utility class. A single source of truth drives both Tailwind utilities and bespoke CSS. The parent reference for Tailwind v4 conventions is [[frontend/tailwind]]; for the CSS custom property substrate see [[frontend/css-custom-properties]].

## Declare tokens in `@theme`, not in raw `@layer base`

Place design tokens inside the `@theme` block at the top of your entry CSS file. Tailwind registers them as utilities automatically.

```css
@import "tailwindcss";

@theme {
  --color-brand: oklch(0.72 0.18 264);
  --color-ink: oklch(0.2 0 0);
  --color-surface: oklch(0.98 0 0);
  --font-display: "Schibsted Grotesk", system-ui, sans-serif;
  --radius-card: 0.75rem;
  --shadow-card: 0 1px 3px rgb(0 0 0 / 0.1);
}
```

This produces `bg-brand`, `text-ink`, `bg-surface`, `font-display`, `rounded-card`, and `shadow-card` without any extra configuration. Putting the same properties in `@layer base` skips the utility generation step and is a common migration mistake from v3.

## Use oklch for color tokens

Define color tokens with `oklch(lightness chroma hue)` instead of hex or `rgb`. The oklch color space is perceptually uniform: stepping lightness by 10% looks like the same visual change across every hue. Use [[frontend/css-custom-properties]] patterns for fallback values if you need to support older browsers, though evergreen browsers have shipped oklch since 2023.

```css
@theme {
  --color-brand-100: oklch(0.95 0.05 264);
  --color-brand-500: oklch(0.72 0.18 264);
  --color-brand-900: oklch(0.35 0.14 264);
}
```

The scale becomes `bg-brand-100`, `bg-brand-500`, and `bg-brand-900`. Keep scales short. A full 50 to 950 scale for every hue multiplies your token count and your build output.

## Reference tokens from bespoke CSS without duplication

Because `@theme` writes every token to `:root` as a CSS custom property, bespoke CSS reads them directly.

```css
.prose blockquote {
  border-left: 4px solid var(--color-brand-500);
  padding-inline-start: 1rem;
  color: var(--color-ink);
}
```

This means third-party markup you cannot annotate with classes, like a CMS article body, still participates in the design system. There is no secondary token definition file.

## Keep the token set small and intentional

Every token in `@theme` generates a utility. Fifty color tokens times five utility families (bg, text, border, ring, outline) is 250 utility classes in the output. Remove unused tokens to keep the build small; the scanner only prunes utilities that never appear in source, not tokens that appear in `@theme` but only via `var()` in bespoke CSS.

Rules of thumb for a tight token set:

- Colors: brand scale (3 to 5 steps), neutral scale (3 to 5 steps), semantic names (danger, success, warning).
- Spacing: override the default scale only if the design uses a non-standard base unit.
- Typography: one display font if needed; body font only if the default stack is not right.
- Radius: card, button, input. Three values cover most UIs.

## Support runtime theme switching with CSS custom property overrides

`@theme` writes to `:root`. Override the same property on a scoped selector or in a media query to switch themes at runtime without a build step.

```css
@theme {
  --color-surface: oklch(0.98 0 0);
  --color-ink: oklch(0.2 0 0);
}

[data-theme="dark"] {
  --color-surface: oklch(0.12 0 0);
  --color-ink: oklch(0.92 0 0);
}
```

A JavaScript one-liner toggles `data-theme="dark"` on the root element. All Tailwind utilities that use the token (`bg-surface`, `text-ink`) update immediately. No class rewriting or style recalculation beyond what the browser's cascade already does. For the full dark mode strategy, see [[frontend/tailwind-dark-mode]].

## Bridge the design system from Figma token exports

Design tools like Figma export tokens as JSON or CSS variables. Map the export to `@theme` in a single translation file rather than scattering raw values through components.

```css
/* tokens.css — generated from Figma, committed to version control */
@theme {
  --color-primary: oklch(0.72 0.18 264);
  --color-secondary: oklch(0.68 0.15 190);
  --space-unit: 0.25rem;
  --radius-base: 0.375rem;
}
```

Import this file before your component CSS. When the design system updates, only `tokens.css` changes; no component files need touching.

## Avoid token drift with a quarterly audit

Token drift is the most common long-term failure mode: two tokens for the same color, a spacing token that nothing uses, a font token that duplicates the default. Run a search for `--` in your entry CSS file quarterly and delete anything with no non-`@theme` references. The build will not catch this for you.

## Related

- [[frontend/tailwind]]
- [[frontend/css]]
- [[frontend/css-custom-properties]]
- [[frontend/tailwind-dark-mode]]
- [[frontend/tailwind-variants]]
- [[comparisons/tailwind-vs-css-modules]]
