DEV Community

Malahim Haseeb
Malahim Haseeb

Posted on • Originally published at blog.malahim.dev

Tailwind CSS v4 — What Actually Changed (And What It Means for Your Next.js Project)

Tailwind v3 had a good run. Drop in tailwind.config.js, point the content array at your files, wire up PostCSS — done. Then v4 shipped in early 2025 and it's not an incremental update. It's a rethink.

I've been running v4 in a real Next.js project (QuickPU result portal) and this is what actually changed in practice.


The Biggest Shift — CSS-First Configuration

In v3, your design system lived in JavaScript. In v4, it lives in CSS.

v3 setup — two config files, PostCSS required:

// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: { brand: '#6366f1' },
    },
  },
  plugins: [],
}

// postcss.config.js
module.exports = {
  plugins: { tailwindcss: {}, autoprefixer: {} },
}
Enter fullscreen mode Exit fullscreen mode

v4 setup — one CSS file, zero JS config:

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

@theme {
  --color-brand: oklch(62% 0.19 264);
  --font-sans: "Inter", sans-serif;
}
Enter fullscreen mode Exit fullscreen mode

Content detection is automatic. No content array. No missed files.

Why this matters: Your design tokens are no longer trapped in a JS build step — they're real CSS variables at runtime. DevTools can read them. Other CSS can reference them. The design system becomes part of the cascade.

Note for Next.js users: You still need a thin postcss.config.mjs with @tailwindcss/postcss. The "zero config" story is fully true for Vite — Next.js needs that one extra file.


oklch Colors — The Palette Is Different Now

v4 ships with a brand-new default palette built in oklch — a perceptually uniform color space that maps closer to how the human eye perceives color.

v3 — static hex at build time:

.bg-blue-500 { background-color: #3b82f6; }
/* No CSS variable. No runtime access. */
Enter fullscreen mode Exit fullscreen mode

v4 — live CSS custom property:

:root { --color-blue-500: oklch(62.3% 0.214 259); }
.bg-blue-500 { background-color: var(--color-blue-500); }
/* Visible in DevTools. Overridable at runtime. */
Enter fullscreen mode Exit fullscreen mode

Two practical wins from this:

Dark mode gets simpler:

@theme {
  --color-bg: oklch(98% 0 0);
  --color-text: oklch(15% 0 0);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: oklch(12% 0 0);
    --color-text: oklch(95% 0 0);
  }
}
/* One override. Everything updates automatically. */
Enter fullscreen mode Exit fullscreen mode

Opacity modifiers always work:

/* v3 — sometimes broke with CSS variables */
bg-blue-500/50  /* unpredictable */

/* v4 — oklch has a dedicated alpha channel */
bg-blue-500/50  /* always correct */
bg-brand/30     /* works on custom theme colors too */
Enter fullscreen mode Exit fullscreen mode

The @theme Directive — Your Entire Design System in CSS

@theme replaces theme.extend entirely. Define CSS variables with the right naming prefix and Tailwind generates utilities automatically.

@import "tailwindcss";

@theme {
  /* --color-* → bg-, text-, border-* utilities */
  --color-primary: oklch(62% 0.19 264);
  --color-surface: oklch(97% 0.008 264);

  /* --spacing-* → p-, m-, gap-* utilities */
  --spacing-18: 4.5rem;

  /* --font-* → font-* utilities */
  --font-display: "Cal Sans", "Inter", sans-serif;

  /* --radius-* → rounded-* utilities */
  --radius-card: 1rem;

  /* --shadow-* → shadow-* utilities */
  --shadow-card: 0 1px 3px oklch(0% 0 0 / 8%), 0 4px 16px oklch(0% 0 0 / 6%);
}

/* You can now use: bg-primary, p-18, font-display, rounded-card, shadow-card */
Enter fullscreen mode Exit fullscreen mode

The prefix is the convention. Tailwind reads it and generates the right utilities.


Build Speed — The Oxide Engine

v3 (PostCSS) v4 (Oxide)
Full build ~180ms ~35ms
Incremental (HMR) ~50–100ms <1ms

The full build number (~5× faster) looks good in benchmarks. The HMR number is what you feel every day — sub-millisecond on every save, compounding across hundreds of saves per session.

What powers it:

  • Lightning CSS — Rust-based CSS parser, replaces PostCSS
  • Automatic content detection — no content array, no missed files
  • Incremental by design — only reprocesses what changed

New Utilities Worth Knowing

Utility What it does In v3?
field-sizing-content Textarea auto-grows with content. No JS resize hack. No
not-* not-hover:opacity-50 — apply when variant is NOT active No
inert Style elements with the inert attribute No
nth-child / nth-last nth-3:bg-muted — target specific children No
@container Container queries built in, no plugin needed Plugin only
starting @starting-style — animate from display:none without JS No
@utility Custom utilities that work with hover:, dark:, md: etc. @layer only

Migrating a Real Next.js Project

I migrated QuickPU from v3 to v4. The actual process:

Step 1 — Install:

npm install tailwindcss @tailwindcss/postcss
Enter fullscreen mode Exit fullscreen mode
// postcss.config.mjs
export default {
  plugins: { '@tailwindcss/postcss': {} },
}
Enter fullscreen mode Exit fullscreen mode

Step 2 — Update your CSS entry point:

/* Before (v3) */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* After (v4) */
@import "tailwindcss";

@theme {
  --color-primary: oklch(62% 0.19 264);
  --font-sans: "Inter", sans-serif;
  /* ...rest of your tokens */
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — Run the official codemod:

npx @tailwindcss/upgrade
Enter fullscreen mode Exit fullscreen mode

It handles: config → @theme conversion, @tailwind@import, renamed utilities (shadow-smshadow-xs, etc.), deprecated class names.

What the codemod won't fix:

  • Visual regressions from renamed utilities — always do a UI pass after
  • Third-party libraries (shadcn/ui, Headless UI) may need manual reconciliation with your @theme
  • Custom plugin() functions — still work via compat mode, but @utility / @variant are the v4-native alternatives

For a medium Next.js project: expect 30–60 minutes, not days.


Should You Migrate?

Stay on v3 if:

  • You ship to production soon and it's already working — don't introduce risk
  • A third-party UI library you depend on hasn't confirmed v4 compatibility
  • You have extensive custom plugin logic

Migrate to v4 if:

  • Starting a new project in 2025/2026 — no reason to start on v3
  • You want design tokens as actual CSS variables (DevTools visibility, runtime theming)
  • You're building a dark-mode-first or heavily themed UI
  • HMR speed matters in your daily workflow

Key Takeaways

  1. This is an architectural shift, not a version bump. CSS-first config means your design system is part of the cascade — not a JS abstraction on top of it.
  2. oklch is genuinely better than hex for UI work. Perceptually uniform, P3-wide, and opacity always works.
  3. The codemod covers ~90% of migration. Run it, do a visual pass, fix edge cases.
  4. New projects should start on v4 today. The v3 config overhead is gone.
  5. <1ms HMR is the quiet win. ~5× faster full builds looks good on paper, but sub-millisecond incremental is what changes your daily dev experience.

Full post with side-by-side code comparisons and more detail on the QuickPU migration at blog.malahim.dev.

— Malahim Haseeb · AI & Full Stack Developer · malahim.dev

Top comments (0)