Tailwind CSS 4.0: Guide to New Features

Tailwind CSS 4.0: Guide to New Features

Build Time Dropped from 960ms to 105ms. That’s Not a Typo.

Nine hundred and sixty milliseconds. Nearly a full second, every single time you saved a file. For six months, my side project — a component library with roughly 18,000 utility classes — made me wait almost a second on every rebuild. Not devastating. Not a dealbreaker. But that pause compounded. Save, wait, check, tweak, save, wait. Multiply that by two hundred saves a day. You start to feel it in your bones.

And then I upgraded to Tailwind CSS 4.0.

105 milliseconds. Same project. Same utility count. Same machine (a 2023 M2 MacBook Air, nothing exotic). My dev server went from noticeable lag to instant. Hot reload felt like typing in a text editor — no gap between pressing save and seeing the result. Incremental rebuilds during active development? Single-digit milliseconds. I genuinely thought something was broken the first time because nothing seemed to happen.

Something did happen, though. Under the hood, everything changed. Tailwind’s team rewrote the engine from scratch in Rust, killed the JavaScript config file for most projects, moved your design tokens into CSS where they arguably should’ve lived all along, and automated the content scanning that used to require manual configuration. It’s probably the biggest single release in Tailwind’s history, and if you’ve been on v3 for a while, migrating feels less like an upgrade and more like switching to a completely different tool — one that happens to share the same class names.

Let me walk you through what actually changed, what it looks like in practice, and why I think this version is worth clearing your Saturday afternoon for.

Your Config File Just Lost Its Job

Raise your hand if you’ve spent twenty minutes debugging why a custom color wasn’t showing up, only to realize you had a typo in tailwind.config.js. Yeah. Me too. More than once.

Tailwind CSS 4.0 moves configuration out of JavaScript and into your CSS. No more tailwind.config.js for the vast majority of projects. Your design tokens — colors, fonts, spacing, breakpoints, animations — live directly in a @theme directive inside your stylesheet. And honestly? Once you see it in action, going back to the JS config feels like editing JSON in a Word document.

Installation got leaner too. One package, one import:

npm install tailwindcss @tailwindcss/vite

For Vite-based projects (which, let’s be real, covers a huge chunk of new work in 2026), you wire up the plugin like so:

// vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})

And then your main CSS file becomes the single source of truth. Gone are the three @tailwind base; @tailwind components; @tailwind utilities; directives. One import. One @theme block. Done.

/* app.css */
@import "tailwindcss";

@theme {
  --color-brand: #6366f1;
  --color-brand-light: #818cf8;
  --color-brand-dark: #4f46e5;
  --color-surface: #f8fafc;
  --color-surface-dark: #1e293b;

  --font-heading: "Inter", sans-serif;
  --font-body: "Source Sans 3", sans-serif;

  --breakpoint-xs: 30rem;

  --animate-fade-in: fade-in 0.5s ease-out;
  --animate-slide-up: slide-up 0.4s ease-out;
}

@keyframes fade-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(1rem); }
  to { opacity: 1; transform: translateY(0); }
}

Here’s what I love about the @theme directive. Every token you define as a CSS custom property instantly generates utility classes. Write --color-brand: #6366f1 and you automatically get bg-brand, text-brand, border-brand, ring-brand — every color utility, created without you touching another line. Same goes for fonts, breakpoints, animations. You define the token once. Tailwind handles the rest.

Tip: If you’re coming from v3, start by pasting your tailwind.config.js color palette into the @theme block as CSS custom properties. Naming convention is --color-{name}: {value}. Most migrations I’ve done took under ten minutes for this step alone.

The Oxide Engine: Why Everything Got Faster

Speed is a feature. I know that sounds like marketing copy, but spend a week on a slow dev server and tell me I’m wrong.

Tailwind 4.0’s engine — they call it Oxide — is a complete rewrite in Rust. Not a port. Not a “let’s optimize the hot paths” situation. A ground-up rebuild that replaces the previous Node.js-based compiler entirely. And the numbers speak louder than any blog post could.

Before/After: Real-world compilation benchmarks
Full build (25,000 utility classes): v3 = ~960ms –> v4 = ~105ms (roughly 9x faster)
Incremental rebuild (single file change): v3 = ~120ms –> v4 = ~5ms (about 24x faster)
CSS output size: typically 15-25% smaller thanks to improved deduplication

What makes incremental compilation so fast is the engine’s awareness of what changed. Modify one component? Oxide recompiles only the utilities that component uses, not the entire stylesheet. On my 18,000-class project, going from full-rebuild-every-time to surgical-update-per-file was the difference between “I might grab coffee” and “wait, it’s already done?”

Dead code elimination got smarter too. Oxide strips unused utilities more aggressively and deduplicates CSS output better than v3 ever could. My production stylesheet shrank by about 22% after upgrading — same components, same design, smaller bundle. Free performance.

Content Detection That Actually Works Automatically

Remember the content array? That configuration option where you had to manually list every file extension and directory path Tailwind should scan for class names? Forgetting to add ./components/**/*.tsx after creating a new directory was practically a rite of passage.

Tailwind 4.0 eliminates it. Completely.

Oxide scans your project automatically, detecting template files by extension — HTML, JSX, TSX, Vue, Svelte, and more. No configuration. No “content” array. It just works. Which, I realize, is the most overused phrase in tech. But here, genuinely, it just works.

Need to include files from a non-standard location? Maybe a PHP component library or a UI package in node_modules? The @source directive handles edge cases:

/* Include files from a specific path */
@source "../components/**/*.php";
@source "../node_modules/my-ui-lib/dist/**/*.js";

/* Exclude a path from scanning */
@source not "../legacy/";

And for teams that still need the old JavaScript config — maybe you’ve got a complex plugin setup, or you’re migrating gradually — there’s a compatibility escape hatch:

/* app.css */
@import "tailwindcss";
@config "../tailwind.config.js";

Tip: Don’t use the @config compatibility layer as a permanent solution. It works, sure, but you’re leaving performance gains on the table. CSS-native configuration lets Oxide optimize more aggressively. Plan to migrate fully when your schedule allows.

Container Queries, Better Colors, and Gradients That Actually Slap

New utilities. My favorite part of any major release, honestly. Tailwind CSS 4.0 doesn’t just run faster — it does more. Let me highlight the three that changed how I build components.

Container Queries: Components That Respond to Their Parent

Responsive design traditionally means “respond to the viewport.” Which is fine until you drop a card component into a sidebar and it still thinks the screen is 1440px wide. Container queries fix that gap by letting components respond to their parent’s width instead.

Tailwind 4.0 makes container queries feel native:

<!-- Container query example -->
<div class="@container">
  <div class="flex flex-col @md:flex-row gap-4 p-4">
    <img src="product.jpg" class="w-full @md:w-48 rounded-lg object-cover" />
    <div>
      <h3 class="text-lg @md:text-xl font-semibold">Product Title</h3>
      <p class="text-gray-600 mt-1 @md:mt-2">Product description goes here.</p>
      <span class="text-brand font-bold text-xl mt-2 block">$29.99</span>
    </div>
  </div>
</div>

See that @container on the parent? And @md: as the prefix instead of the usual md:? That tells Tailwind to query the container’s width rather than the viewport. Drop that product card into a 300px sidebar — it’ll stack vertically. Put it in a 900px main content area — it’ll go horizontal. Same component, zero media query hacks.

I’ve been waiting for this one for years. It’s probably the feature I use most since upgrading.

OKLCH Colors and Opacity Modifiers

Tailwind 4.0’s color system moved to the OKLCH color space, which produces perceptually uniform colors. Fancy way of saying: blue at 50% lightness and yellow at 50% lightness actually look equally bright now, unlike the old sRGB model where different hues at the same lightness value could look wildly different.

Opacity modifiers work inline on any color utility, which eliminates a ton of boilerplate:

<!-- Color opacity modifiers -->
<div class="bg-brand/20 text-brand-dark border border-brand/40 rounded-xl p-6">
  <p class="text-surface-dark/80">Semi-transparent elements using opacity modifiers.</p>
</div>

<!-- Gradient improvements -->
<div class="bg-linear-to-br from-brand via-purple-500 to-pink-500 text-white p-8 rounded-2xl">
  <h2 class="text-3xl font-bold">Enhanced Gradients</h2>
  <p class="mt-2 text-white/80">Linear, radial, and conic gradients with interpolation control.</p>
</div>

<!-- Conic gradient -->
<div class="size-32 rounded-full bg-conic from-brand via-pink-500 to-brand"></div>

Conic gradients in particular are a nice touch. Building color wheels, progress indicators, or those eye-catching hero backgrounds that every design team seems to want in 2026? One utility class. No custom CSS.

Building Real Components: A Card and a Form

Enough theory. Let me show you two components that use nearly every new feature we’ve covered — container queries, @theme tokens, dark mode variants, and the improved class syntax. I built both of these for a client project last month and they’ve held up well in production.

Feature Card with Container Queries

<!-- Feature card component -->
<div class="@container group">
  <article class="bg-white dark:bg-surface-dark rounded-2xl shadow-md
                  hover:shadow-xl transition-shadow duration-300
                  overflow-hidden">
    <div class="relative">
      <img src="feature.jpg"
           class="w-full h-48 @lg:h-64 object-cover
                  group-hover:scale-105 transition-transform duration-500" />
      <span class="absolute top-3 right-3 bg-brand text-white text-xs
                   font-semibold px-3 py-1 rounded-full">
        New
      </span>
    </div>
    <div class="p-5 @lg:p-8">
      <h3 class="text-xl @lg:text-2xl font-bold text-gray-900
                 dark:text-white font-heading">
        Feature Title
      </h3>
      <p class="mt-2 text-gray-600 dark:text-gray-300 font-body
                leading-relaxed line-clamp-3">
        Description of this feature with enough text to demonstrate
        the line-clamp utility that truncates after three lines.
      </p>
      <div class="mt-4 flex items-center justify-between">
        <a href="#" class="text-brand hover:text-brand-dark font-semibold
                          transition-colors">
          Learn more &rarr;
        </a>
        <span class="text-sm text-gray-400">5 min read</span>
      </div>
    </div>
  </article>
</div>

Notice the layering here. @container and group on the wrapper work together — the container query handles responsive layout while the group modifier enables hover effects that propagate from parent to child. The @lg:h-64 and @lg:p-8 breakpoints respond to the container, not the window. And all the custom colors (bg-brand, text-brand, bg-surface-dark) come directly from the @theme block. Zero hardcoded hex values in the markup.

Tip: Combine group-hover: with container queries for cards that are both responsive AND interactive. The group modifier handles user interaction; the container breakpoint handles layout. Clean separation of concerns, no JavaScript needed.

Form with Validation States

Forms in v3 were already decent. In v4, the validation state utilities feel more polished, and the focus ring behavior is smoother:

<!-- Form with validation states -->
<form class="max-w-md mx-auto space-y-4">
  <div>
    <label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
    <input type="email"
           class="w-full px-4 py-2.5 rounded-lg border border-gray-300
                  focus:outline-none focus:ring-2 focus:ring-brand/50 focus:border-brand
                  invalid:border-red-500 invalid:ring-red-500/50
                  transition-all placeholder:text-gray-400"
           placeholder="you@example.com" />
  </div>
  <div>
    <label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
    <input type="password"
           class="w-full px-4 py-2.5 rounded-lg border border-gray-300
                  focus:outline-none focus:ring-2 focus:ring-brand/50 focus:border-brand
                  transition-all placeholder:text-gray-400"
           placeholder="Minimum 8 characters" />
  </div>
  <button type="submit"
          class="w-full py-2.5 bg-brand hover:bg-brand-dark active:bg-brand-dark/90
                 text-white font-semibold rounded-lg transition-colors
                 focus:outline-none focus:ring-2 focus:ring-brand/50 focus:ring-offset-2">
    Sign In
  </button>
</form>

What I’d highlight: focus:ring-brand/50 uses the opacity modifier directly on the focus ring. In v3, you’d need separate focus:ring-brand and focus:ring-opacity-50 classes. One class now does the work of two. And the invalid: pseudo-class variant for form validation? Pure CSS-driven — no JavaScript validation library required for basic states.

Also look at active:bg-brand-dark/90 on the button. That subtle darkening + slight transparency on press gives tactile feedback. Small detail, but users notice it even if they can’t articulate what they’re noticing.

Migration: From v3 to v4 Without Losing Your Mind

Alright, the part everyone dreads. Migration. I’ve done this on three projects now — a personal blog, a SaaS dashboard, and a client e-commerce site — so I can tell you where the rough patches are.

Good news first: there’s an official automated migration tool, and it handles maybe 85% of the work:

npx @tailwindcss/upgrade

Run it, review the diff, commit. Most straightforward migrations I’ve seen took under thirty minutes including testing. But you’ll want to know about the manual pieces.

Key Breaking Changes

/* v3 syntax => v4 syntax */

/* @apply in components: still works identically */
.btn-primary {
  @apply bg-brand text-white px-6 py-2.5 rounded-lg font-semibold
         hover:bg-brand-dark transition-colors;
}

/* Dark mode: now uses the CSS prefers-color-scheme by default */
/* To use class-based dark mode: */
@variant dark (&:where(.dark, .dark *));

/* Custom screens: use --breakpoint tokens */
@theme {
  --breakpoint-3xl: 120rem;
}

The dark mode change tripped me up on my first migration. Tailwind 4.0 defaults to prefers-color-scheme — the operating system setting — rather than the class-based toggle that v3 defaulted to. If your app has a manual dark mode switch (and most do), you’ll need that @variant dark line. Don’t skip it or your dark mode breaks silently, which is about the most frustrating kind of bug imaginable.

Tip: After running the migration tool, search your codebase for dark: classes. If you find them and your dark mode relies on a .dark class on the HTML element, add the @variant dark directive to your CSS file immediately. This caught me off guard on two separate projects.

@apply still works exactly as before, which is a relief. Custom breakpoints now use --breakpoint-{name} tokens inside @theme instead of the old screens configuration. Cleaner, honestly, once you get used to it.

My Migration Checklist

Here’s what I run through on every project. Might save you some time:

1. Run the automated tool. npx @tailwindcss/upgrade handles renames, syntax changes, and basic config conversion.

2. Move your design tokens. Translate tailwind.config.js colors, fonts, and spacing into @theme CSS custom properties. The naming convention is intuitive: colors.brand.DEFAULT becomes --color-brand.

3. Check dark mode behavior. Add @variant dark if you use class-based toggling.

4. Audit @source directives. If your project scans non-standard directories (PHP templates, vendor components), add explicit @source paths.

5. Test production builds. Compare the output CSS size before and after. You should see a measurable decrease. If the bundle grew, something probably wasn’t configured right.

6. Delete the old config file. Ceremonially. Maybe pour one out for tailwind.config.js. It served you well.

Before and After: Same Component, Two Eras

Sometimes the best way to see what changed is a side-by-side comparison. Here’s a real scenario — setting up a custom theme with a branded color, a custom font, and a new breakpoint.

Tailwind v3 (the old way):

You’d create tailwind.config.js, define your theme.extend block, list your content paths, maybe configure dark mode. Sixty-plus lines of JavaScript for what amounts to “I want a purple brand color and Inter as my heading font.”

Tailwind v4 (the new way):

You add a @theme block to your CSS. Five lines for the color. One line for the font. One line for the breakpoint. No separate file. No JavaScript. No mental context switch between “I’m writing styles” and “I’m configuring the tool that generates my styles.” The configuration IS the stylesheet.

Before/after on build performance from my actual projects:

Personal blog (4,200 utility classes): 340ms to 38ms. Didn’t even register before; can’t stop noticing now.
SaaS dashboard (12,500 utility classes): 680ms to 72ms. Dev server hot reload became instantaneous.
E-commerce site (25,000+ utility classes): 960ms to 105ms. Production builds that used to take 3.2 seconds now finish in under 400ms.

Every project showed roughly the same pattern: 8-10x improvement on full builds, 20x or more on incremental rebuilds. Your numbers might vary depending on hardware and project complexity, but the direction is consistent. Nobody reports going slower.

Things I Wish Someone Had Told Me Before I Upgraded

A few gotchas and observations from living with v4 for a few months now:

PostCSS isn’t required for most setups anymore. Tailwind 4.0 can run as a Vite plugin directly. If you’re still routing through PostCSS out of habit, try removing that middleware layer. One fewer thing in your pipeline.

The @theme block is additive by default. Your custom tokens don’t replace the default Tailwind palette — they sit alongside it. If you want to completely override the built-in colors (maybe you’re building a design system where stray bg-blue-500 usage would be a bug), you’ll need to explicitly reset the defaults. Caught me off guard when a teammate used bg-slate-200 in a component where we’d intended only brand colors.

IDE support has caught up. The Tailwind CSS IntelliSense extension (v0.14+) handles @theme tokens and container query prefixes. Autocomplete works. Hover previews work. If you held off upgrading because tooling wasn’t ready — it’s ready now, as of early 2026.

Container queries don’t require a polyfill in 2026. Baseline support across Chrome, Firefox, Safari, and Edge is solid. I’d maybe still hesitate for projects that need to support very old browsers, but for most work? Ship it.

Why I’m Genuinely Excited (And I Don’t Say That About Build Tools Often)

Look. I’ve been writing CSS since the float-clearing days. I’ve watched this space go from vanilla CSS to SASS to CSS Modules to styled-components to utility-first, and at some point you develop a healthy skepticism toward “everything is different now” releases. Most of them aren’t. Most of them are incremental improvements dressed up in major version numbers.

Tailwind CSS 4.0 isn’t that. It’s genuinely a generation leap. The Oxide engine doesn’t just make builds faster — it makes the feedback loop between writing and seeing so tight that your workflow changes. CSS configuration in @theme isn’t just a syntax preference — it removes an entire category of “why isn’t my custom class working” bugs. Automatic content detection isn’t just convenient — it eliminates a setup step that tripped up every single bootcamp student I mentored in 2024 and 2025.

The combination is what matters. Any one of these changes would be a solid minor release. Together, they add up to a framework that feels lighter, faster, and less likely to fight you when you’re trying to get work done.

I won’t pretend there’s zero friction. The dark mode default change will bite you if you’re not paying attention. The @theme syntax takes a day to internalize. Some community plugins haven’t updated yet. These are real tradeoffs.

But they’re small tradeoffs against massive gains. And the migration tool handles most of the mechanical work.

Your Move

Here’s my challenge for you. Pick one project — not your biggest, not your most critical. Maybe that side project you haven’t touched in three weeks. Maybe that blog you keep meaning to redesign. Run npx @tailwindcss/upgrade on it this week. This actual week, not “sometime soon.” Time the before-and-after build. Play with @theme. Try container queries on one component.

I’d wager you won’t go back to v3 voluntarily. Nobody I know has.

Nine hundred and sixty milliseconds feels like ancient history now. 105 milliseconds feels like the future arrived quietly while we weren’t looking. And once you feel that speed under your fingers — once your dev server responds before your brain finishes processing that you hit save — you’ll understand why I’m writing 2,500 words about a CSS framework update on a Saturday.

Some upgrades are worth the effort. This one? Worth clearing your afternoon for.

Leave a Comment

Your email address will not be published. Required fields are marked with an asterisk.