@theme, CSS Variables and v4 CSS-First Configuration

Advanced
13 min

@theme, CSS Variables and v4 CSS-First Configuration

Tailwind v4 moved configuration out of JavaScript and into CSS: design tokens are CSS variables declared inside @theme, and every utility, variant and IntelliSense suggestion is generated from them. This lesson explains the system underneath — namespaces, extending versus overriding, the inline and static options, and using theme values from ordinary CSS — so you can design a complete token system without a config file.

How @theme works

@theme looks like a block of custom properties, but Tailwind treats each name as an instruction: the prefix (the namespace) decides which utilities are produced.

| Namespace | Utilities generated | |---|---| | --color-* | bg-*, text-*, border-*, ring-* and every other color utility | | --font-*, --text-*, --font-weight-*, --tracking-*, --leading-* | family, size, weight, letter-spacing, line-height | | --breakpoint-*, --container-* | responsive variants, container variants, max-w-* | | --spacing | every spacing and sizing utility (p-4 = --spacing × 4) | | --radius-*, --shadow-*, --ease-*, --animate-* | rounded-*, shadow-*, ease-*, animate-* |

Variables must sit inside @theme at the top level of the processed CSS file, never nested under a selector or media query. A plain :root { --brand: … } produces no utilities.

Extending, overriding and resetting

A new name extends the theme; a default name overrides it; initial clears a namespace:

css
@theme { --color-brand: #2563eb; /* extend: adds bg-brand, text-brand … */ --breakpoint-2xl: 100rem; /* override: changes the 2xl: breakpoint */ --color-lime-*: initial; /* reset one family: lime-* utilities disappear */ --font-*: initial; /* reset a namespace, then define your own */ --font-sans: "Inter", sans-serif; }

--*: initial wipes the entire default theme — the starting point for a strict design system. Sub-properties use a double dash: --text-hero--line-height attaches a line height to the text-hero size.

Theme variables are real CSS variables

Everything in @theme is emitted as a custom property on :root, so custom CSS and JavaScript share the tokens the utilities use:

css
.card { border-radius: var(--radius-card); box-shadow: var(--shadow-md); color: var(--color-gray-900); padding: --spacing(6); /* 1.5rem */ background: --alpha(var(--color-brand) / 10%); /* brand at 10% */ }

--spacing() and --alpha() are compile-time functions that resolve to the values the utilities use. Tailwind emits only the variables that are referenced; use @theme static { … } when a variable must always exist, for example because JavaScript reads it with getComputedStyle. Because var() does not work inside @media, write @media (width >= theme(--breakpoint-md)) for breakpoints in custom CSS.

@theme inline and runtime theming

Normally a utility references the variable: .bg-brand { background-color: var(--color-brand); }. When the theme value itself points at another variable that changes per element, @theme inline makes the utility embed the value directly so the indirection resolves where the class is used:

css
:root { --background: #ffffff; --foreground: #0f172a; } .dark { --background: #0f172a; --foreground: #f8fafc; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); }

This is the pattern create-next-app generates: raw values live on :root and .dark, the theme maps them to tokens, and bg-background text-foreground switches when the dark class is toggled — runtime theming with one set of utilities.

A legacy tailwind.config.js can still be loaded with @config "../tailwind.config.js"; as a migration aid; its values are not exposed as CSS variables.

Common mistakes

  • Putting @theme inside @layer, a selector or a media query; it must be top-level.
  • Declaring --brand-500 without a namespace and expecting bg-brand-500 to exist.
  • Forgetting @theme static when JavaScript reads a token no utility uses.
Quick Quiz
Question 1 of 2

Which declaration makes a `font-display` utility available?

Key Takeaways

  • @theme variables are grouped by namespace (--color-*, --font-*, --breakpoint-* …), and each namespace generates its own utilities.
  • New names extend the theme, existing names override it, and --namespace-*: initial resets it.
  • Every token is a CSS variable on :root; use var(), --spacing() and --alpha() in custom CSS and theme() inside @media.
  • @theme inline enables runtime theming via :root/.dark variables; @theme static emits every variable.
  • @config loads a legacy JavaScript config during migration only.

Next lesson: Reusable Components and @apply — extract repeated utility combinations into component classes.

@theme, CSS Variables and v4 CSS-First Configuration - Tailwind CSS | CodeYourCraft | CodeYourCraft