Tailwind v3 to v4: The Pitfalls of the Upgrade Tool in Detail
AI generated
</>
tw
Tailwind CSS · Upgrade Tool · v3 to v4
Tailwind v3 to v4
the pitfalls of the upgrade tool in detail

The official Tailwind upgrade tool promises an automated switch from v3 to v4 with a single command. In practice the codemod handles the common cases reliably, but stumbles on custom plugins, safelist configurations and deprecated utilities that quietly behave differently than before, without any warning.

18 min read Tailwind CSS v3 · v4 · @tailwindcss/upgrade Codemod · Config migration · Breaking changes

1. What the official upgrade tool solves automatically

The Tailwind upgrade tool, invoked through npx @tailwindcss/upgrade, is a codemod that automates the mechanical parts of a v3 to v4 migration: renamed utilities, changed default values, and converting the JavaScript configuration into Tailwind's new CSS-first format. For many small to medium projects, a single run is genuinely enough to make most of the codebase functional again. The promise of a seamless switch, however, only holds for the part of the migration that can be described purely syntactically.

This is exactly the key point when using the Tailwind upgrade tool: it reliably recognizes patterns that reduce to plain search and replace, such as renamed classes like flex-shrink-0 to shrink-0. But it cannot make semantic decisions that depend on individual project structure, such as how custom plugins should be carried over to the new utility registration system. Anyone who runs the tool blindly and does not carefully review the diff unknowingly merges faulty or incomplete replacements into the main branch.

2. Prerequisites: Node version, git status and backup

Before the Tailwind upgrade tool is even started, three prerequisites should be met. First, a clean git status with no uncommitted changes, so the entire codemod diff remains visible as a single, reviewable commit. Second, Node 20 or newer, since the tool relies on modern JavaScript features and fails unpredictably with older Node versions, sometimes without a meaningful error message. Third, a separate migration branch, so the main branch stays untouched until the manual follow-up is complete.

A frequently overlooked pitfall at this stage: the tool does not only modify CSS and config files, it also scans every template file for Tailwind classes and rewrites them. On very large codebases with thousands of templates, this step can take several minutes and should not be interrupted, since an aborted run of the Tailwind upgrade tool can leave files in an inconsistent intermediate state.


# Recommended sequence before running the official upgrade tool
git status --porcelain   # must be empty — commit or stash first
node --version            # must be 20.x or newer
git checkout -b tailwind-v4-migration

# Run the codemod — always inspect the diff afterwards, never trust blindly
npx @tailwindcss/upgrade

# Review every changed file before committing
git diff --stat
git diff app/design/frontend/Mironsoft/default/web/tailwind/tailwind.css

3. Config.js to CSS-first: what the codemod migrates correctly

The core step of every v4 migration is converting tailwind.config.js into a CSS-first configuration using the @theme directive. The Tailwind upgrade tool reliably handles standard cases such as theme.extend.colors and theme.extend.spacing, because these have a direct, lossless equivalent as CSS custom properties. screens definitions are also usually translated correctly into @theme breakpoint variables.

Things get harder with configurations that contain JavaScript logic instead of plain values, such as computed color palettes from a function or conditional values based on environment variables. The Tailwind upgrade tool cannot automatically translate these cases into static CSS, because @theme only accepts static values. The codemod often marks such spots with a comment, but skips the actual translation entirely, which is easily missed on a superficial review of the diff.


// tailwind.config.js — BEFORE (v3)
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          DEFAULT: "#0369a1",
          light: "#7dd3fc",
        },
      },
      // PITFALL: computed value — the upgrade tool cannot translate this
      spacing: Object.fromEntries(
        Array.from({ length: 20 }, (_, i) => [i + 1, `${(i + 1) * 0.25}rem`])
      ),
    },
  },
};

/* tailwind.css — AFTER codemod (v4) — computed spacing was NOT migrated */
@import "tailwindcss";

@theme {
  --color-brand: #0369a1;
  --color-brand-light: #7dd3fc;

  /* MANUAL WORK REQUIRED: the computed spacing scale from config.js
     had to be written out explicitly by hand, one value per line */
  --spacing-1: 0.25rem;
  --spacing-2: 0.5rem;
  --spacing-3: 0.75rem;
  /* ... remaining values written out manually ... */
}

4. Pitfall: custom plugins are not automatically translated

Arguably the biggest pitfall when using the Tailwind upgrade tool concerns custom plugins registered through plugin(function ({ addUtilities, addComponents }) { ... }). Tailwind v4 still supports the old plugin API for the most part, but the recommended approach for new utilities has changed fundamentally: instead of registering JavaScript functions, custom utilities are now preferably defined directly as CSS through @utility. The upgrade tool does not migrate this part automatically, because it has no reliable way of translating arbitrary JavaScript code into equivalent CSS.

In practice this means: every project with more than a handful of custom plugins should build its own inventory of all plugin files before running the Tailwind upgrade tool. After the automated run, these plugins remain functional because v4 continues to support the legacy API, but they lose the performance and maintenance benefits of the new @utility syntax unless they are migrated manually as well.


/* MANUAL migration example — from JS plugin to native @utility (v4) */

/* BEFORE (v3 plugin, not touched by the upgrade tool):
   plugin(function ({ addUtilities }) {
     addUtilities({
       '.text-shadow-sm': { textShadow: '0 1px 2px rgba(0,0,0,0.15)' },
     });
   });
*/

/* AFTER — written manually, native v4 syntax */
@utility text-shadow-sm {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);
}

5. Pitfall: safelist and dynamic classes go undetected

Tailwind's content scanning only recognizes class names when they appear as a complete string in the source code. Projects that assemble classes dynamically from variables, such as bg-${color}-500, often rely on a safelist entry in tailwind.config.js in v3 to still include these classes in the final CSS. The Tailwind upgrade tool does carry over the safelist mechanism structurally, but does not check whether the classes listed there still carry the same names in the new utility system.

Particularly tricky: if a utility was renamed between v3 and v4, such as overflow-ellipsis to text-ellipsis, the old name stays in the safelist without the upgrade tool issuing any warning. The result is a safelist class that never appears in the final CSS, because it is no longer recognized as a valid utility anywhere in the source code. This pitfall often only surfaces when a component suddenly appears unstyled in production, because the dynamically assigned class resolves to nothing.

6. Pitfall: deprecated utilities that quietly behave differently

Not every change between v3 and v4 is a rename the Tailwind upgrade tool can reliably detect. Some utilities keep their name but change their default behavior. A well known example is the default ring color and width, which changed between versions. Since the class name stays identical, there is no text pattern difference for the codemod to replace, yet the behavior still changes when rendered in the browser.

Such silent behavior changes are more dangerous than obvious breaking changes, because they trigger no build error and the diff produced by the Tailwind upgrade tool remains unremarkable at these spots too. The only reliable way to catch them is a visual regression test before and after the migration, ideally automated through screenshot comparisons of the most important page types, instead of relying on the git diff alone.

7. PostCSS configuration and build tool adjustments

Tailwind v4 uses a new engine written in Rust called Oxide, which also changes its integration into build tools. The Tailwind upgrade tool does update postcss.config.js to the new @tailwindcss/postcss package, but it does not check whether other PostCSS plugins in the same project are compatible with the new engine. Plugins that rely on internal implementation details of the old JavaScript engine, for example to manipulate generated CSS after the fact, can silently stop doing anything after the upgrade, with no visible error.

An additional point concerns Vite and Webpack configurations that previously referenced specific paths to the Tailwind CLI or the PostCSS plugin. Since package names and export paths partly changed between v3 and v4, the Tailwind upgrade tool does perform the plain config file adjustment, but leaves custom build scripts outside standard configuration files untouched, which in individually customized setups leads to build failures that only surface after the actual codemod run.

8. Manual follow-up: a checklist after the automated run

After every run of the Tailwind upgrade tool, a fixed checklist is worth having to systematically work through the known pitfalls instead of discovering them in production. This includes checking all custom plugin files for remaining legacy API usage, a targeted look at every safelist class with dynamic name generation, and a visual comparison of the most important pages before and after the migration.

Equally important is a build run in an isolated environment before the migration branch is merged, because many problems with the Tailwind upgrade tool only surface when the CSS bundle is actually compiled, not already during the codemod run itself. A CI job that compares the bundle byte for byte before and after the migration reveals unexpected size changes that can hint at overlooked, no longer working utilities.

9. Automated vs. manual compared

The following table classifies the most common migration tasks by whether the Tailwind upgrade tool automates them reliably or whether manual follow-up remains unavoidable.

Migration task Automated by the tool Manual follow-up
Renamed utility classes Yes, reliably Spot-check is usually enough
Static colors/spacing in config Yes, reliably Cross-check the @theme value diff
Computed/dynamic config values No Write out values manually in @theme
Custom plugins (addUtilities) No Convert manually to @utility
Safelist with renamed classes No, no warning Check every safelist entry individually
Silent behavior changes (e.g. ring) No, no diff signal Visual regression test needed

The pattern is clear: everything that can be described as a text pattern is automated reliably by the Tailwind upgrade tool. Everything that requires semantic understanding of the project, meaning computed values, custom plugin logic or dynamically generated class names, remains the responsibility of the development team.

Mironsoft

Tailwind CSS upgrades, build tooling and frontend architecture for Magento and Hyvä

Upgrading from v3 to v4 without being blindsided by pitfalls?

We carry out your Tailwind upgrade, review every codemod diff manually, and handle the follow-up work on custom plugins, safelist classes and silent behavior changes that the official tool does not resolve automatically.

Upgrade audit

Upfront analysis of all custom plugins, safelist entries and dynamic classes

Codemod review

Manual review of every diff after the tool run, no blind merges

Regression testing

Visual comparison of the most important pages before and after the upgrade

10. Summary

The Tailwind upgrade tool is a valuable starting point for the v3 to v4 migration, but not a complete substitute for an attentive development team. It reliably handles renamed classes and static configuration values, but fails on computed config values, custom plugins and safelist entries with outdated class names, without ever issuing a warning.

Anyone who treats the Tailwind upgrade tool as the first step of a multi-stage process rather than a complete solution avoids the typical pitfalls. A fixed checklist with a plugin audit, safelist check and visual regression test after the automated run makes the difference between a clean migration and unpleasant surprises in production.

Tailwind v3 to v4 upgrade tool: the key points at a glance

What works reliably

Renamed utilities and static config values are translated correctly into @theme.

Biggest pitfall

Custom plugins using addUtilities are not automatically converted to @utility.

Silent danger

Behavior changes without a rename, such as ring defaults, produce no diff and no warning.

Safeguard

Visual regression test and plugin checklist after every automated run.

11. FAQ: Tailwind v3 to v4 Upgrade Tool

1How do I start the upgrade tool?
With npx @tailwindcss/upgrade on a clean git branch with Node 20 or newer.
2Does it migrate the whole config automatically?
Only static values, computed values must be written manually into @theme.
3What happens to custom plugins?
Keep working, but should be manually converted to @utility.
4Why did a safelist class disappear?
Renamed utilities leave the old name in the safelist, silently, without a warning.
5Is the git diff enough on its own?
No, silent behavior changes additionally need a visual regression test.
6Do I need to adjust PostCSS manually?
Standard adjustment is automatic, custom plugins need a manual compatibility check.
7How long does it take on large projects?
Several minutes on thousands of templates, should not be interrupted.
8What is the safest way to run it?
Separate branch, manual diff review, run through a checklist, then merge.
9Does it detect JavaScript logic in the config?
Partially marked with a comment, but translation is skipped.
10Apply it directly in production?
No, always test isolated first and adopt only after manual follow-up.