# Extend the Theme with @theme

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

> MUST: Use theme.extend to add custom values; never overwrite top-level theme keys

Declare design tokens in a CSS @theme block, which extends the defaults instead of replacing them

This is the single largest v4 breaking change and the one most stale advice still gets wrong. There is no `tailwind.config.js` by default, and `theme.extend` does not exist — the JS config object is gone. Tokens are plain CSS custom properties declared inside `@theme { --color-brand: oklch(0.62 0.19 259); }`, and that block extends by default: your brand color lands next to slate, red, and every other built-in. Which means the entire v3 failure mode this principle used to be about — "you wrote `theme.colors` instead of `theme.extend.colors` and nuked the palette" — is structurally impossible now. Clobbering is opt-in, not accidental: to drop the defaults you must explicitly reset the namespace with `--color-*: initial;` before declaring your own, which is the escape hatch for teams that want a closed palette where `bg-red-500` should not silently work. Every token you declare doubles as a real CSS variable, so `var(--color-brand)` resolves anywhere, including inside a third-party stylesheet.

## Bad — do not do this

`design-theme-config-bad`

```tsx
export function ThemeConfigBad() {
  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">tailwind.config.js</h4>
        <p className="text-xs text-muted-foreground mb-3">
          v3 API. A v4 project has no config file, so nothing loads this.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-error">{`// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: { brand: 'oklch(0.62 0.19 259)' },
    },
  },
}`}</pre>
        </div>
      </div>

      <div className="bg-card border border-border rounded-lg p-4 space-y-2">
        <h5 className="text-sm font-medium">What v4 does with it</h5>
        <ul className="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>
              <code className="font-mono">theme.extend</code> does not exist in v4. This object is never read.
            </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>
              No <code className="font-mono">bg-brand</code> utility is generated — the class silently does nothing.
            </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>No build error. You find out when the button renders transparent.</span>
          </li>
        </ul>
      </div>

      <p className="text-xs text-error">
        Extend-vs-overwrite was a v3 problem. This config answers a question v4 no longer asks.
      </p>
    </div>
  );
}
```

## Good — do this

`design-theme-config-good`

```tsx
export function ThemeConfigGood() {
  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">app.css</h4>
        <p className="text-xs text-muted-foreground mb-3">
          Tokens are CSS custom properties. <code className="font-mono">@theme</code> extends the defaults.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`@import "tailwindcss";

@theme {
  --color-brand: oklch(0.62 0.19 259);
}`}</pre>
        </div>
        <div className="mt-3 flex flex-wrap items-center gap-2 text-xs">
          <span className="rounded bg-background border border-border px-2 py-1 font-mono">bg-brand</span>
          <span className="text-muted-foreground">generated, and every default still works</span>
        </div>
      </div>

      <div className="bg-card border border-border rounded-lg p-4">
        <h5 className="text-sm font-medium mb-1">Opt in to a reset</h5>
        <p className="text-xs text-muted-foreground mb-3">
          Clobbering is deliberate now: reset the namespace to <code className="font-mono">initial</code> first.
        </p>
        <div className="bg-muted rounded-md p-3 font-mono text-xs overflow-x-auto">
          <pre className="text-foreground">{`@theme {
  --color-*: initial;        /* drop the built-ins */
  --color-brand: oklch(0.62 0.19 259);
  --color-surface: oklch(0.97 0 0);
}`}</pre>
        </div>
        <p className="text-xs text-muted-foreground mt-2">
          Now <code className="font-mono">bg-red-500</code> no longer exists — a closed palette, on purpose.
        </p>
      </div>

      <p className="text-xs text-success">
        @theme adds to the defaults by default. You cannot wipe the palette by accident.
      </p>
    </div>
  );
}
```

## References

- [Tailwind v4: Theme variables](https://tailwindcss.com/docs/theme)
- [Tailwind v4: Upgrade guide](https://tailwindcss.com/docs/upgrade-guide)
