# Tokens Go in @theme, Not :root

**MUST** · **ID:** `design-theme-not-root` · **Category:** design
**Source:** [Tailwind](https://tailwindcss.com/docs)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-theme-not-root

> MUST: Declare design tokens in `@theme`, not `:root` — a variable in `:root` is a working custom property but Tailwind never sees it, so it generates NO utilities and the codebase fills with `bg-[var(--brand)]` arbitrary values. `@theme { --color-brand: … }` generates `bg-brand`/`text-brand`/`border-brand`/`ring-brand` AND still emits the custom property for `var()`. The namespace prefix does the work: `--color-*` feeds color utilities, `--spacing-*` spacing, `--font-*` families, `--breakpoint-*` breakpoints. Nothing errors when you get it wrong.

Declare design tokens inside @theme so they generate utilities, instead of in :root where they only ever produce arbitrary values

This is the most common v4 onboarding mistake, and it is invisible: nothing errors, nothing warns. You write `:root { --brand: #06c; }` because that is how CSS custom properties have always worked, and it does define a working variable — but Tailwind never sees it, so no `bg-brand` exists. The codebase then fills with `bg-[var(--brand)] text-[var(--brand)] border-[var(--brand)]`, arbitrary values that carry no variant ergonomics and no autocomplete, and every reviewer reads them as noise. Move the same declaration into `@theme` under the right namespace — `@theme { --color-brand: oklch(0.62 0.19 259); }` — and Tailwind generates `bg-brand`, `text-brand`, `border-brand`, `ring-brand`, and the rest, while still emitting `--color-brand` as a real custom property that `var()` resolves anywhere. The namespace prefix is what does the work: `--color-*` feeds color utilities, `--spacing-*` spacing, `--font-*` families, `--breakpoint-*` breakpoints. A token in the wrong namespace, or in none at all, generates nothing.

## Rule snippet

```tsx
/* generates nothing */ :root { --brand: oklch(0.62 0.19 259); }
/* generates bg-brand, text-brand, … */ @theme { --color-brand: oklch(0.62 0.19 259); }
```

## Bad — do not do this

`design-theme-not-root-bad`

```tsx
export function ThemeNotRootBad() {
  return (
    <div className="w-full max-w-md space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="font-medium mb-1">Declared in :root</h4>
        <p className="text-xs text-muted-foreground mb-3">
          A perfectly valid CSS variable. Tailwind never sees it.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-error">{`:root {
  --brand: oklch(0.62 0.19 259);
}`}</pre>
        </div>
      </div>

      <div className="bg-card border border-border rounded-lg p-4">
        <h5 className="text-sm font-medium mb-1">What the markup turns into</h5>
        <p className="text-xs text-muted-foreground mb-3">
          No <code className="font-mono">bg-brand</code> exists, so every call site pays in arbitrary values.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-error">{`<button class="bg-[var(--brand)] ring-[var(--brand)]">
<span   class="text-[var(--brand)]">
<div    class="border-[var(--brand)]">`}</pre>
        </div>
        <ul className="mt-3 text-xs space-y-2">
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-error" />
            <span>No autocomplete, no hover docs — the token is invisible to tooling.</span>
          </li>
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-error" />
            <span>Nothing errors. It works, it is just noise forever.</span>
          </li>
        </ul>
      </div>

      <p className="text-xs text-error">
        :root defines a variable. It does not define a utility.
      </p>
    </div>
  );
}
```

## Good — do this

`design-theme-not-root-good`

```tsx
export function ThemeNotRootGood() {
  return (
    <div className="w-full max-w-md space-y-4">
      <div className="bg-card border border-border rounded-lg p-4">
        <h4 className="font-medium mb-1">Declared in @theme</h4>
        <p className="text-xs text-muted-foreground mb-3">
          Same value. The <code className="font-mono">--color-*</code> namespace is what generates utilities.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`@theme {
  --color-brand: oklch(0.62 0.19 259);
}`}</pre>
        </div>
      </div>

      <div className="bg-card border border-border rounded-lg p-4">
        <h5 className="text-sm font-medium mb-1">What the markup turns into</h5>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto mt-3">
          <pre className="text-foreground">{`<button class="bg-brand ring-brand hover:bg-brand/90">
<span   class="text-brand">
<div    class="border-brand">`}</pre>
        </div>
        <ul className="mt-3 text-xs space-y-2">
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-success" />
            <span>Variants, opacity modifiers, and autocomplete all work.</span>
          </li>
          <li className="flex items-start gap-2">
            <span className="mt-1.5 w-1.5 h-1.5 shrink-0 rounded-full bg-success" />
            <span>
              Still a real custom property: <code className="font-mono">var(--color-brand)</code> resolves anywhere.
            </span>
          </li>
        </ul>
      </div>

      <div className="bg-card border border-border rounded-lg p-4">
        <h5 className="text-sm font-medium mb-2">The namespace does the work</h5>
        <div className="space-y-1.5 text-xs">
          <div className="flex items-center gap-2">
            <code className="font-mono bg-muted px-1.5 py-0.5 rounded">--color-*</code>
            <span className="text-muted-foreground">bg-, text-, border-, ring-</span>
          </div>
          <div className="flex items-center gap-2">
            <code className="font-mono bg-muted px-1.5 py-0.5 rounded">--spacing-*</code>
            <span className="text-muted-foreground">p-, m-, gap-, w-</span>
          </div>
          <div className="flex items-center gap-2">
            <code className="font-mono bg-muted px-1.5 py-0.5 rounded">--font-*</code>
            <span className="text-muted-foreground">font-</span>
          </div>
        </div>
      </div>

      <p className="text-xs text-success">
        A token in @theme is both a CSS variable and a set of utilities. In :root it is only the first.
      </p>
    </div>
  );
}
```

## References

- [Tailwind v4: Theme variables](https://tailwindcss.com/docs/theme)
- [Tailwind CSS v4.0 announcement](https://tailwindcss.com/blog/tailwindcss-v4)
