Skip to main content
Back to Blog
5 min read

Tailwind 4 migration from Tailwind 3: the 90-minute path

I migrated a 40-component Next.js project from Tailwind 3 to Tailwind 4 in 90 minutes. Here's the step-by-step, what broke, what the codemod missed, and the one gotcha that cost me 20 minutes.

next.jsperformance

Tailwind 4 is a significant rewrite — new engine, CSS-first configuration, no more tailwind.config.js by default. That sounds scary. In practice, the migration on a real 40-component Next.js project took 90 minutes. Most of it was straightforward. One gotcha cost me 20 minutes.

Here's the exact path.

Step 1: Run the codemod (15 minutes)

Tailwind provides an official upgrade tool:

npx @tailwindcss/upgrade

This handles the bulk of the migration:

On my project, the codemod handled about 85% of the changes automatically. It modified 28 files and the global CSS file.

Important: commit before running the codemod, so you can see exactly what it changed and revert if needed.

Step 2: Fix what the codemod missed (30 minutes)

The codemod is good but not perfect. Here's what I had to fix manually:

Custom colors in tailwind.config.js

My project had custom colors defined with CSS custom properties:

// Old: tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        accent: 'var(--color-accent)',
        surface: 'var(--color-surface)',
      },
    },
  },
}

The codemod moved this to the CSS file, but the syntax for referencing CSS variables changed:

/* New: globals.css */
@theme {
  --color-accent: var(--color-accent);
  --color-surface: var(--color-surface);
}

This creates a circular reference. The fix: use the actual color values in @theme, or define your CSS variables separately and reference them:

:root {
  --color-accent: oklch(0.7 0.15 200);
  --color-surface: oklch(0.15 0.02 260);
}

@theme {
  --color-accent: var(--color-accent);
  --color-surface: var(--color-surface);
}

@apply with variants

Some @apply usage with variants needed manual adjustment. The codemod missed cases where @apply was inside media queries or pseudo-selectors.

Dynamic class names

If you construct class names dynamically (template literals, clsx conditions), the codemod can't detect them. I had to manually check components that build class strings.

Step 3: The gotcha — content configuration (20 minutes)

This is what cost me 20 minutes. In Tailwind 3, you configure which files to scan for class names:

// tailwind.config.js
module.exports = {
  content: ['./app/**/*.{ts,tsx}', './components/**/*.{ts,tsx}'],
}

Tailwind 4 auto-detects content sources — it scans all files imported from your CSS entry point. In theory, you don't need a content config.

In practice, if you have classes in files that aren't directly imported (like MDX content, or utility files that aren't in the import tree), Tailwind 4 won't find them and those styles won't be generated.

My MDX blog posts use Tailwind classes in custom components. The codemod removed the content config, and suddenly blog post styles were missing. The fix:

/* globals.css */
@source "../content/**/*.mdx";

The @source directive tells Tailwind 4 to scan additional paths. Check your rendered pages after migration — if styles are missing, a @source directive probably needs to be added.

Step 4: Verify (25 minutes)

After the code changes:

  1. npm run build — check for compilation errors. Tailwind 4 is stricter about some class patterns.
  2. Visual check on every page. Open the dev server and click through every page. Look for:
    • Missing colors (custom theme values not migrated)
    • Spacing changes (some default values shifted slightly)
    • Shadow/blur/ring changes (naming convention updated)
    • Dark mode issues (if using class strategy)
  3. Lighthouse quick check. Tailwind 4's new engine generates smaller CSS. On my project, the CSS bundle dropped from 28KB to 19KB (32% reduction). Lighthouse performance score went up 2 points.

What's actually better in Tailwind 4

Beyond the migration, here's why the upgrade is worth it:

Faster builds. The new Oxide engine is written in Rust. On my project, CSS compilation went from ~400ms to ~80ms. Noticeable in dev server restarts.

Smaller output. The new engine is more aggressive about tree-shaking unused styles. 20–35% smaller CSS in my experience.

CSS-first config. Defining your theme in CSS (instead of JS) means your IDE can autocomplete custom values and show previews. The feedback loop is faster.

Native cascade layers. Tailwind 4 uses @layer natively, which means better interaction with third-party CSS and fewer specificity issues.

What to watch out for

Plugin compatibility. If you use Tailwind plugins (typography, forms, container-queries), check that they have Tailwind 4 compatible versions. The plugin API changed.

tailwind.config.js still works. If you have complex config (plugins, custom utilities), you can keep the JS config file. Tailwind 4 supports both CSS-first and JS config. Migration doesn't have to be all-or-nothing.

PostCSS changes. Tailwind 4 uses its own PostCSS pipeline. If you have custom PostCSS plugins, test that they still work with the new setup.

The timeline

StepTime
Run codemod15 min
Fix codemod misses30 min
Fix content/source config20 min
Visual verification25 min
Total90 min

For a larger project (100+ components, heavy customization), budget 2–3 hours. For a small project (landing page, portfolio), 30–45 minutes.

The migration is worth doing. Faster builds, smaller CSS, and Tailwind 4 is where the ecosystem is moving. The longer you wait, the larger the diff.


Need help migrating your project to Tailwind 4? Let's talk — I keep client projects on current tooling so they don't accumulate tech debt.