A team I worked with last quarter spent four hours upgrading a Next.js app to Tailwind v4. The framework migration script ran cleanly. The build worked. Then their design system package, a private fork of shadcn/ui frozen in 2024, refused to compile because it imported tailwind.config.js from a path that no longer existed. That config file had been ported into CSS by the upgrade tool, but the dependency reached past the codebase boundary to read it directly. The fix took another day. The real cost was the discovery: every package that touched the v3 config in any non-standard way needed to be rewritten.
This is the part of Tailwind v4 that does not show up in the benchmark posts. The Oxide engine is fast, the CSS-first config is cleaner, and the unified Lightning CSS toolchain removes whole categories of PostCSS configuration. None of that is in dispute. What is in dispute, more than a year after the stable release, is whether the surrounding ecosystem has caught up enough to make the migration safe for an arbitrary production codebase.
What v4 actually retired
The headline change is the disappearance of tailwind.config.js. In v3, that file was the source of truth for theme tokens, content paths, plugins, and variants. In v4, the equivalent lives inside a CSS file, declared with @theme and a set of CSS custom properties:
@import "tailwindcss";
@theme {
--color-brand-500: oklch(0.62 0.19 256);
--font-display: "Satoshi", "sans-serif";
--breakpoint-3xl: 1920px;
--ease-snappy: cubic-bezier(0.2, 0, 0, 1);
}Every token defined inside @theme becomes both a generated utility class and an actual CSS custom property in the output. The variable --color-brand-500 generates bg-brand-500, text-brand-500, border-brand-500, and so on. It also ends up in the runtime as var(--color-brand-500), which means components can read it from outside the utility system. The design token and the utility class are now the same object.
This change goes deeper than syntax. It is the framework formally acknowledging that design tokens belong in CSS, not in a JavaScript build step. Older Tailwind setups had to choose: keep tokens in the config and lose runtime access, or duplicate them into a :root block and accept drift. The v4 model collapses that into one declaration.
Three other v3 staples are gone or replaced:
- The
contentarray. v4 auto-detects template files by walking the project tree and excluding gitignored paths. You can still pin source paths explicitly using the@sourcedirective, but the default works for most projects. - The
variantsobject. Custom variants now use@custom-variantdirectly in CSS. - The JS
plugin()API. Most plugins now express themselves as plain CSS using@utilityand@custom-variant. A JS plugin shim still exists for backwards compatibility, but anything maintained is moving to CSS.
The PostCSS configuration also disappears. Tailwind v4 ships its own integrated pipeline built on Lightning CSS, which handles @import resolution, vendor prefixing, nesting flattening, and modern CSS down-leveling. Most projects can delete postcss.config.js, autoprefixer, and postcss-import entirely. For Vite projects, the recommended path is the dedicated @tailwindcss/vite plugin, which bypasses PostCSS altogether and is noticeably faster than the PostCSS bridge.
The CSS-first config in practice
Moving config into CSS sounds like a syntactic change. It is not. It alters the mental model in three ways that matter once you are building real components.
First, theme tokens become composable through CSS scope. You can override --color-brand-500 inside a .dark selector, a media query, or a @layer block, and the utility that consumes it picks up the new value automatically. In v3, switching theme values at runtime required either swapping classes or maintaining a parallel CSS variable layer that mirrored your config. The new model removes that duplication. A dark-mode override is now:
@theme {
--color-surface: oklch(0.99 0 0);
--color-on-surface: oklch(0.18 0 0);
}
@media (prefers-color-scheme: dark) {
:root {
--color-surface: oklch(0.18 0 0);
--color-on-surface: oklch(0.96 0 0);
}
}Second, OKLCH replaces hex as the default color space. The full default palette ships in OKLCH, and color-mixing operations such as opacity modifiers (bg-brand-500/40) use native CSS color-mix() at runtime instead of computing a flattened color at build time. The visible result is that adjacent shades stay perceptually uniform: brand-300 to brand-400 is the same perceived contrast step as the gap between any other two adjacent shades, which is not true for hex-based palettes.
Third, @utility lets you define new utilities in the same language as the rest of the stylesheet. A custom truncate-on-two-lines utility used to require a JS plugin. Now it is:
@utility line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}These are not new capabilities. v3 plugins could do the same things. What is new is that the surface area available to a non-plugin author has expanded to cover almost every reason someone wrote a plugin in v3.
Oxide and the build pipeline
Oxide is the rewritten engine that powers v4. The class scanner and dependency walker are written in Rust, and the CSS generation core is faster TypeScript that benefits from the cleaner architecture. Tailwind Labs measured roughly 3.5x faster full builds and over 100x faster incremental rebuilds on their own benchmark suite. In real projects, the incremental number matters more: HMR-driven workflows now feel CSS-instant on codebases where v3 had a perceptible delay.

The integration with Lightning CSS is the second piece. Lightning CSS does the work that autoprefixer, postcss-import, postcss-nested, and a handful of polyfill plugins used to cover. It is bundled, not a peer dependency, which means there is no version drift between Tailwind and the CSS processor. The cost of that bundling is that you cannot swap Lightning CSS for an alternative, and you inherit whatever browser-targeting decisions Lightning CSS makes.
Browser targeting is set in two places. The @tailwindcss/postcss and @tailwindcss/vite plugins both read from your browserslist configuration, and the Tailwind framework itself has a hard floor at Safari 16.4, Chrome 111, and Firefox 128. The floor is a deliberate design decision: v4 leans on @property, color-mix(), and registered custom properties to make features like opacity modifiers and theme overrides work without build-time compilation. There is no realistic way to back-port that. If you need pre-2023 Safari support, v4 is not for you, and v3.4 will continue to be maintained as a fallback track.
The migration path the docs do not show
Tailwind ships an upgrade tool: npx @tailwindcss/upgrade. It does most of the mechanical work. It converts the v3 config to CSS, updates dependency versions, swaps deprecated class names (shadow-sm to shadow-xs, ring defaulting to 1px instead of 3px, and so on), and rewrites your @tailwind directives to the single @import "tailwindcss". For a clean, self-contained app, this is a one-command migration that finishes in under a minute.
For anything else, there is a list of edges that the tool does not handle, and reading them in advance saves a day of debugging.
| v3 pattern | v4 reality |
|---|---|
| JS config imported by other packages (Storybook, design-system libs, generated docs) | These need a small tailwind.config.js shim that re-exports tokens, or migration of consumers to read from CSS variables. |
Plugins using addUtilities, matchUtilities | JS plugin API still works in compat mode, but new plugins should use @utility. Plugin authors are slowly republishing. |
Custom container configuration | Removed. Replace with @utility container block setting margin-inline and padding-inline directly. |
| Arbitrary hex colors in shared design tokens | Still allowed, but mixing hex and OKLCH in one palette produces visibly inconsistent shade ramps. Convert the whole palette or none of it. |
Tailwind class names in CSS files via @apply | Still supported. @apply inside CSS Modules and Vue scoped styles requires the @reference directive to point at your main stylesheet. |
The @reference requirement catches almost every monorepo. If your component package compiles its own CSS in isolation, the v4 scanner has no visibility into the global theme, so @apply bg-brand-500 silently produces nothing. Adding @reference "../app.css" at the top of each scoped stylesheet fixes it, but you have to know to do it.
The plugin ecosystem is still in transition
More than a year after stable release, the third-party landscape is uneven. Tailwind's own plugins (typography, forms, container queries) shipped v4-native versions early. Headless UI updated. shadcn/ui has a v4-compatible CLI and most of its components work, though some recipes still assume the v3 config shape. DaisyUI, NextUI, and a long tail of community plugins range from fully ported to abandoned.
The pattern is consistent: anything that wrapped Tailwind's JS plugin API needed a meaningful rewrite, not a search-and-replace. Plugin authors who built thin layers over addUtilities could ship v4 builds quickly. Plugins that did serious work inside the JS API (custom variant logic, dynamic theme generation, runtime token resolution) often took months, and a few have simply not shipped.
AI tooling has its own version of this lag. Code completion models trained before 2025 will happily generate v3 patterns: tailwind.config.js with a theme.extend block, module.exports plugin arrays, @tailwind base; @tailwind components; @tailwind utilities; at the top of the stylesheet. The completions compile under the v3 compatibility shim but obscure the actual v4 idiom. Teams using AI-assisted scaffolding need either to lean on Tailwind's own examples in their prompts or to accept that the first scaffold will be v3-shaped and require editing.
IDE tooling has fared better. The official Tailwind CSS IntelliSense extension for VS Code understands both v3 and v4 projects and detects the config shape automatically. Prettier's prettier-plugin-tailwindcss works for v4 with current versions. JetBrains IDE support landed shortly after stable. The dev surface is fine. The supply chain underneath it is the part that takes time.
The browser baseline is a strategic decision, not a technical one
The Safari 16.4 / Chrome 111 / Firefox 128 floor sounds modest until you map it to enterprise reality. Safari 16.4 shipped in March 2023, which means anyone running an iOS device on iOS 15 (millions of older iPhones that Apple cut from iOS 16) is excluded. Chrome 111 also shipped in March 2023, but Chrome's auto-update reaches a long tail of locked-down corporate environments slowly. Firefox 128 is more recent and rarely a problem because Firefox auto-updates.
Tailwind's framing is that v4 is a forward-looking tool and that v3.4 remains supported for projects that cannot move yet. That is honest. It is also a real decision the team must make. The framework no longer pretends to be universal. The bet is that the browser features v4 depends on, @property, color-mix(), registered custom properties, and full nesting support, are worth requiring because they remove categories of build-time complexity that have plagued the framework for years.
For consumer products with a long device tail (e-commerce, banking, public sector), this decision needs an actual analytics check, not a vibe. Look at your real Safari version distribution. If you have 4% of traffic on iOS Safari below 16.4, that is not necessarily a blocker, but it is a number that should be argued about explicitly before the upgrade lands.
What this means for design systems
The CSS-first config is, in practice, a design-token system. That makes v4 the first version of Tailwind that competes directly with the design-token-as-CSS-variables approach used by tools like Style Dictionary and @radix-ui/themes. A v4 theme is portable: any consumer that can read CSS custom properties can read your tokens, whether that consumer is another framework, a Web Components library, or a non-Tailwind page.
Teams that already had a Style Dictionary pipeline producing CSS variables face a new question: keep the pipeline, or let Tailwind own the token contract directly. The answer depends on how many non-web outputs you generate. If your tokens fan out to iOS, Android, and Figma, Style Dictionary still earns its keep. If they only fan out to web surfaces, the Tailwind v4 @theme block is the simpler source of truth.

There is also a more interesting structural shift. Component libraries built on v4 ship CSS-first themes that consumers can override by redeclaring custom properties. shadcn/ui's v4 templates already do this: you can replace the entire color palette by overriding a handful of --color-* variables in the consumer's stylesheet, without touching component source. That is a more conventional contract than v3's JS-config override pattern, and it composes with non-Tailwind code in a way the v3 model never did.
The honest conclusion
Tailwind v4 is a better tool than v3 on almost every axis a senior frontend engineer would care about: faster, less configuration surface, cleaner token model, better integration with native CSS, more honest about being a CSS framework rather than a JavaScript framework that happens to produce CSS. The migration is not the hard part. The hard part is everything connected to your codebase that assumed the v3 shape: design system packages, Storybook integrations, Figma sync scripts, AI scaffolding, internal plugins. Those need to be rewritten, and the cost of that rewrite is not visible in the official upgrade guide.
If you are starting a new project in 2026, use v4. If you are running a v3 codebase that builds and ships and is not actively blocking work, the migration is worth scheduling, not rushing. Plan a week for an app you know well, more for a monorepo, and put real time on the calendar for the audit of every package that touches Tailwind's config in any non-default way. The framework moved cleanly. The ecosystem around it is moving on a different clock.
