Skip to main content
CodeAlchemy
Tailwind

Tailwind CSS v4.0 is here: engine, CSS-first config, Play

CodeAlchemy Team··9 min read

Source: Tailwind CSS blog

The v4.0 announcement is unusually direct about the scope of the rewrite:

Holy shit it's actually done — we just tagged Tailwind CSS v4.0.

For something that ships under a familiar name, Tailwind v4 is more rewrite than upgrade. The engine is new, the configuration story is new, and the CSS surface picks up almost a decade of platform features that landed since v3 was first cut. The old utility mental model is preserved — you still write flex items-center gap-4 rounded-lg p-4 — but everything underneath is different enough that the team treats it as a different framework that happens to be backwards compatible.

TL;DR

  • A new engine, "Oxide", with a Rust-accelerated core and Lightning CSS for parsing, vendor prefixing, and @import bundling.
  • CSS-first configuration via @theme blocks. tailwind.config.js is optional for most projects.
  • Automatic content detection — no content array; the compiler walks your project and respects .gitignore.
  • OKLCH palette by default — perceptually uniform color, gradient interpolation in OKLCH, P3 displays handled.
  • Container queries, 3D transforms, conic and radial gradients in core, no plugin.
  • New variantsstarting:, not-*, in-*, details-content: — and modern device queries.

The engine, "Oxide"

v3 was a JavaScript-heavy compiler running through PostCSS. v4 keeps the PostCSS hand-off where it makes sense but pushes the hot paths into a Rust core. Two consequences matter day-to-day:

  • Full builds are 3-5× faster on representative apps from Tailwind Labs benchmarks.
  • Incremental rebuilds drop into the microsecond range when no new utility classes are emitted. The compiler skips work entirely if your edit didn't introduce a new class.

Lightning CSS does the parsing, vendor prefixing, and @import bundling, which means you usually don't need `postcss-import` anymore — @import "./tokens.css" just works. Old toolchain layers (autoprefixer, postcss-nested in places) collapse out of postcss.config.js for typical apps.

CSS-first configuration

The biggest authoring change. Previously, customizing colors meant editing tailwind.config.js:

// v3 — tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: { brand: { 500: "#22c55e" } },
      fontFamily: { display: ["Inter", "sans-serif"] },
    },
  },
};

In v4, the same thing lives in CSS:

@import "tailwindcss";

@theme {
  --color-brand-500: oklch(0.70 0.22 142);
  --font-display: "Inter Display", ui-sans-serif, system-ui;
  --breakpoint-3xl: 120rem;
}

Each variable namespace seeds a category of utilities — --color-* becomes bg-*, text-*, border-*, etc.; --font-* becomes font-*. You can still keep a JS config file for plugins or escape hatches, but most of the surface that used to live there now lives next to the rest of your CSS.

A nice side effect: the theme is real CSS variables. Devtools show them, runtime tools (Storybook, Figma plugins, color-picker extensions) can read them, and you can override them at any scope without recompiling.

Automatic content detection

v3 made you maintain a content array; forget a folder and your component would render unstyled. v4 walks the project automatically:

  • Skips node_modules (most of the time — @source opens it explicitly when needed).
  • Respects .gitignore so build artifacts and large vendored folders don't slow scans.
  • Skips file types it can't parse (binary assets, images).

When automation isn't enough, three new at-rules give you control:

  • `@source "../packages/ui/dist/.js"`** — pull classes out of a sibling package.
  • `@source not "../legacy"` (added in v4.1) — exclude a folder.
  • `@source inline("hidden md:flex")` — safelist runtime-only strings.

The result: a freshly cloned v4 project goes from npm install to working build with zero scanner configuration in 90% of cases.

OKLCH palette

The default palette switches from sRGB hex values to OKLCH — a perceptually uniform color space designed against the same goals as Lab and LCH but tuned for displays. Two practical consequences:

  • The same step on the lightness scale (-500 vs -600) feels visually equivalent across hues. In v3 some hues looked muddy at the same step; the team calibrated by hand.
  • Gradients can interpolate in OKLCH instead of sRGB. The grey middle that ruined many "blue → red" gradients goes away.

Concretely:

<div class="bg-linear-to-r from-blue-500 to-red-500 in-oklch">…</div>

Wider-gamut P3 displays (almost every laptop and phone shipped in the last five years) get the saturated colors they're capable of. sRGB-only displays still render correctly with a fallback.

Gradients and 3D

The gradient utilities expand:

  • Linear: bg-linear-to-r, bg-linear-to-br, plus the interpolation modifier.
  • Conic: bg-conic-* for pie charts, gauges, decorative borders.
  • Radial: bg-radial-* for spotlights and elliptical highlights.

3D transforms ship in core — no plugin needed:

  • rotate-x-*, rotate-y-*, rotate-z-*
  • translate-z-*
  • perspective-*
  • transform-3d to turn on 3D rendering for a stacking context.

Card flips, dimensional hover effects, and modest depth treatments stop requiring hand-rolled CSS.

Container queries in core

Container queries graduated from the @tailwindcss/container-queries plugin to core. Use @container on a wrapper, then style children with @-prefixed variants:

<div class="@container">
  <article class="@md:grid-cols-2 @lg:grid-cols-3 grid grid-cols-1 gap-4">
    …
  </article>
</div>

This was the most-requested missing feature for years. With it in core, design systems can express layout intent at the component level instead of stitching media queries to viewport sizes that may or may not match the component's actual context.

`starting:` variant for `@starting-style`

Discrete property animations (popovers, modals, anything that goes from display: none to visible) used to need JavaScript. The browsers eventually shipped @starting-style. v4 wraps it in a variant:

<div class="opacity-100 transition-opacity starting:opacity-0">
  …
</div>

The element starts at opacity-0 on first render and transitions to opacity-100 automatically. No hooks, no portals, no requestAnimationFrame dance.

Composable variants

The variant grammar got more flexible:

  • `not-*`not-hover:, not-data-active:, etc.
  • `in-*`in-open: for descendants of an open ancestor.
  • `group-not-*`, `peer-not-*` — combine with the existing group/peer system.

These compose: group-hover:not-data-disabled:bg-brand-500 is a real, supported selector that previously required a custom plugin or arbitrary variants.

Plugins folded into core

If you carried these in v3, drop them on upgrade:

  • @tailwindcss/container-queries — now core.
  • @tailwindcss/aspect-ratio — utilities in core.

Plugins still exist for forms, typography, and other opinionated surface area, but the surface that needed plugins shrank.

Migration: what you actually have to do

The published upgrade guide ships an automated tool:

npx @tailwindcss/upgrade

It rewrites @tailwind base/components/utilities to @import "tailwindcss", moves theme overrides from JS to a @theme block, and collapses arbitrary values that can now be expressed as dynamic utilities. What's left for you:

  • Plugins. Custom plugins that hooked v3 internals will need updates; community plugins released v4 builds throughout 2025.
  • Visual regressions. OKLCH ≠ sRGB. Take screenshots before and after. Most sites look slightly more saturated, which is usually an improvement.
  • `tailwind.config.js`. Decide whether to keep it (for plugins, escape hatches) or fully delete it.

Try it without committing

The announcement links to Tailwind Play with v4 enabled — a hosted REPL where you can spike new syntax (@theme, @source, the new variants) without touching your repo. It's the fastest way to confirm a refactor will actually work before you start the upgrade.

DX details that don't fit elsewhere

A grab-bag of small things that don't deserve their own section but matter when you're actually using v4:

  • `bg-(--my-var)` syntax. You can reference an arbitrary CSS variable in any utility position with parentheses, no var() wrapper. Reads cleaner than the v3 arbitrary-value syntax.
  • Dynamic numeric utilities. mt-17, grid-cols-15, w-99 all work without configuration — the compiler emits them on demand. Custom scales for --spacing adjust the base.
  • Color modifiers. bg-brand-500/40 for opacity is unchanged, but composes with the new gradient interpolation: bg-linear-to-r from-brand-500/40 to-brand-700 gives a smooth fade.
  • Default theme stays in CSS. When @theme reference references the default theme, it reads as actual variables you can inspect, not a JS object you have to import.
  • Layer ordering uses the modern @layer rather than the v3-flavored layer comments. Plugins that emit raw CSS without layers cascade as expected.

A note on stability

v4.0 was a big release; v4.1 and v4.2 followed within months without regressions that bit existing users. The cadence the team is signaling — point releases that add features without breaking compatibility — is the same one Vue, React Router, and other mature OSS projects have settled into. Treat the v4 line as production-ready; the alpha window closed before the v4.0 tag.

Where to read more

Source / further reading: Tailwind CSS blog