AI Agents · Tailwind

Tailwind CSS 4 New Features

What's actually new in Tailwind CSS v4: the Rust-based Oxide engine, CSS-first configuration via @theme, native cascade layers, and built-in container queries.

John Kihiu12 min read

Tailwind v4 is the first major version where the build tool changed underneath the utility classes, not just the class names. The old JIT engine, written in JavaScript on top of PostCSS, is replaced with Oxide — a new engine with a Rust core for the hot paths (scanning source files, parsing candidates, generating CSS). The config format changed too: instead of a tailwind.config.js object, most projects now configure their theme directly in CSS. Having migrated two production apps, the real story is less "faster builds" (though it is) and more "less config-file indirection between what you write and what you get."

The Oxide engine and why the speed matters

Tailwind v3's JIT scanner walked your source files with a JS-based tokenizer on every rebuild. Oxide replaces the file-scanning and candidate-extraction step with a Rust implementation, and full builds on large codebases that used to take a few seconds now complete in a few hundred milliseconds in most reports I've seen — the exact multiplier depends heavily on project size and file count, so treat any specific number with suspicion. What's more consequential day-to-day is incremental rebuilds during development: v4's architecture tracks which files changed and only re-scans those, so a single-line edit in a component doesn't re-walk the whole `src` tree.

Zero-config content detection

You no longer list a content: [] glob in a config file. Oxide auto-detects template files by walking the project respecting your .gitignore, so adding a new folder of components doesn't require touching a config array — one less place for a build to silently miss a file.

CSS-first configuration with @theme

The biggest workflow change is that theme customization moves from JavaScript into CSS itself, using an @theme block. Instead of extending a theme.extend.colors object in tailwind.config.js, you declare CSS custom properties inside @theme, and Tailwind generates utility classes from them automatically.

CSS · app.css
@import "tailwindcss";

@theme {
  --color-brand-500: oklch(0.6 0.15 250);
  --font-display: "Cabinet Grotesk", sans-serif;
  --breakpoint-3xl: 120rem;
}

/* bg-brand-500, font-display, and 3xl: are now real utilities */

This matters beyond convenience: because the theme values are plain CSS custom properties, they're readable and overridable at runtime — something a JS config object could never do. You can override --color-brand-500 per-component or per-media-query with ordinary CSS, and every utility built from that variable picks up the change without a rebuild.

Native cascade layers instead of injected specificity hacks

Tailwind v3 managed style precedence with its own internal ordering and occasional !important escape hatches. v4 generates output using native CSS cascade layers (@layer) — base, components, and utilities each get their own layer, and cascade layers have a defined precedence in the CSS spec regardless of selector specificity or source order. This means a single custom-class override no longer needs to out-specificity a utility class; put your override in a later layer (or no layer at all, since un-layered styles always win) and it applies predictably.

Cascade layers need a modern browser baseline

@layer is supported in all current evergreen browsers, but if your project has to support genuinely old browser versions, check your actual analytics before assuming this is a non-issue — cascade layers have no graceful degradation, an unsupported browser just applies every rule at normal cascade order.

Container queries without a plugin

In v3, container queries required the separate @tailwindcss/container-queries plugin. v4 ships them as core utilities: @container on a parent, and @sm:, @md: etc. on children size their styles against the container's width instead of the viewport.

HTML
<div class="@container">
  <div class="grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3 gap-4">
    <!-- columns respond to the container's width, not the viewport -->
  </div>
</div>

This is genuinely useful for component libraries and dashboard widgets that get dropped into sidebars, modals, or main content areas of varying width — a card component can look right whether it's rendered at 300px or 900px wide, without the page's viewport size telling you anything about that.

What migration actually costs

The official upgrade tool (npx @tailwindcss/upgrade) handles the mechanical parts — converting a JS config to an @theme block, renaming a handful of utilities that changed (shadow-sm became shadow-xs, for instance, as the whole shadow/blur/rounded scale shifted by one step). Where I've spent real time is auditing custom plugins and any code that read from theme.config at runtime via resolveConfig() — that JS-introspectable config object still exists for compatibility but isn't the source of truth anymore, so places that relied on it dynamically need a second look.

Wrapping up

Tailwind v4's actual news isn't a longer utility class list — it's that the tool moved closer to plain CSS on two fronts at once: configuration lives in CSS via @theme, and the generated output uses CSS's own cascade-layer mechanism instead of Tailwind's own specificity management. Combined with the Oxide engine's faster scanning, the net effect is a framework that gets out of the way faster and hands more control back to standard CSS features. If your v3 project's config file is mostly color and font extensions, the migration is a weekend, not a rewrite.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.