A complete Tailwind 4.0 theme, in CSS.
@import 'tailwindcss';
@theme {
--color-midnight: oklch(0.15 0.04 265);
--color-neon: oklch(0.72 0.2 330);
--font-display: 'Satoshi', sans-serif;
--breakpoint-3xl: 120rem;
}No tailwind.config.js. No module.exports. No theme.extend nesting. No dev-server restart, because the config is JavaScript and JavaScript is cached. Tailwind 4.0 shipped this week and the config file died with it. I am here to celebrate at the funeral.
For eight years Tailwind was a JavaScript program that emitted CSS. Version 4 is CSS. The content glob array is gone too. Every monorepo got that one wrong once a quarter. Template detection is automatic now, and it respects .gitignore.
The part that compounds: @theme tokens become real custom properties at runtime rather than build-time constants. var(--color-neon) works in handwritten CSS, in inline styles, and in a getComputedStyle read feeding a canvas. Sometimes you want a token out of the cascade, say one derived from another variable. @theme inline inlines the value into the generated utilities and skips the :root variable. Token system and utility system are one system, with an explicit opt-out. The docs bury that distinction, and it is the first thing that bites theme-switching setups.
Under the tokens sits more platform machinery. v4 registers its internal variables with @property, typed syntax and all:
@property --tw-gradient-from {
syntax: '<color>';
inherits: false;
initial-value: #0000;
}Typed registration is why v4 can animate gradients and composed transforms that v3 could only swap between states.
The engine
The numbers come from Tailwind's own table rather than my adjectives. Full builds 378ms → 100ms. Incremental builds with new CSS 44ms → 5ms. Incremental builds that reuse existing classes finish in 192µs. The median rebuild while you code is the third case.
The v4 alpha post from last March explains the machinery. Oxide keeps the framework core in TypeScript and moves the hot paths, scanning and parsing, to Rust. Lightning CSS replaces the entire PostCSS caravan (postcss-import, autoprefixer, postcss-nesting) and does import inlining, nesting, prefixing and syntax lowering in one Rust pass.
One more mechanism, worth knowing before you fight specificity. v4 emits @layer theme, base, components, utilities. Cascade layers lose to unlayered styles by design, so your legacy unlayered CSS always beats Tailwind utilities without !important. The platform adjudicates specificity fights now, and it rules in your favor.
The tradeoff, named honestly
Dynamic configuration got harder. If your theme was computed by JavaScript, say multi-tenant white-labeling or tokens from an API, the config file was your escape hatch. The answer now is "generate the CSS file". Plugins need porting.
The browser floor also rose to Safari 16.4, Chrome 111, Firefox 128, because v4 leans on @property, color-mix() and cascade layers. There is no dead-browser flexibility to negotiate. Check your analytics before you open the migration guide. Colors moved to oklch on the way. Wide-gamut displays are common enough now that the extra chroma is free real estate on good screens, and it clamps gracefully on the rest.
The npx @tailwindcss/upgrade codemod handled this site in one shot, for reference.
There is a pattern this decade. Tools that bootstrapped on JavaScript's flexibility keep returning their borrowed powers to the platform. Sass variables became custom properties. FLIP libraries became view transitions. Tailwind's config becomes @theme. Critics spent years calling this framework "not real CSS". Its entire configuration surface is now real CSS.