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.
@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.
A new name extends the theme; a default name overrides it; initial clears a namespace:
@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.
Everything in @theme is emitted as a custom property on :root, so custom CSS and JavaScript share the tokens the utilities use:
.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.
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:
: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.
@theme inside @layer, a selector or a media query; it must be top-level.--brand-500 without a namespace and expecting bg-brand-500 to exist.@theme static when JavaScript reads a token no utility uses.Which declaration makes a `font-display` utility available?
@theme variables are grouped by namespace (--color-*, --font-*, --breakpoint-* …), and each namespace generates its own utilities.--namespace-*: initial resets it.: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.