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: {} },
}
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;
}
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. */
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. */
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. */
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 */
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 */
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
contentarray, 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
// postcss.config.mjs
export default {
plugins: { '@tailwindcss/postcss': {} },
}
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 */
}
Step 3 — Run the official codemod:
npx @tailwindcss/upgrade
It handles: config → @theme conversion, @tailwind → @import, renamed utilities (shadow-sm → shadow-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/@variantare 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
- 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.
- oklch is genuinely better than hex for UI work. Perceptually uniform, P3-wide, and opacity always works.
- The codemod covers ~90% of migration. Run it, do a visual pass, fix edge cases.
- New projects should start on v4 today. The v3 config overhead is gone.
- <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)