Overview

Migrate in small, reviewable steps; a single-PR rewrite hides regressions in the diff. The v3 to v4 upgrade has an official tool plus a short list of changes it cannot judge for you. The hub is tailwind.

Upgrade v3 to v4 with the official tool

Run it on a new branch (it needs Node.js 20 or newer), then review the diff and test in a browser.

npx @tailwindcss/upgrade

The tool handles the mechanical changes: dependencies, config moved into CSS, @tailwind directives replaced by @import "tailwindcss", and renamed utilities in templates. Compare the old tailwind.config.js against the generated @theme block; anything missing is a gap to port by hand. See tailwind-theme.

Review the changes a codemod cannot judge

  • Renamed utilities: shadow-sm to shadow-xs, shadow to shadow-sm, the same shift for blur, drop-shadow, backdrop-blur, and rounded; outline-none to outline-hidden; ring to ring-3.
  • Removed utilities: bg-opacity-* and its siblings become bg-black/50; flex-shrink-* and flex-grow-* become shrink-* and grow-*; overflow-ellipsis becomes text-ellipsis.
  • Defaults: the default border color is currentColor and ring is 1px, so set border colors explicitly. Buttons use cursor: default, and hover: applies only on hover-capable devices (restore the old behavior with @custom-variant hover (&:hover);).
  • Syntax: the important modifier moves to the end (bg-red-500!); CSS variables in arbitrary values use parentheses (bg-(--brand)); stacked variants apply left to right; prefixes look like variants (tw:flex).
  • Config: container options are gone (extend it with @utility container); safelist becomes @source inline(); a JS config loads only via @config, without corePlugins, safelist, or separator. Plugins are covered in tailwind-plugins.
  • Browsers: v4 targets Safari 16.4+, Chrome 111+, and Firefox 128+. If you must support older browsers, stay on v3.
  • Preprocessors: Sass, Less, and Stylus are not supported; remove them from Tailwind’s pipeline.

Migrate custom CSS leaf-first

Start with the smallest components (buttons, badges, inputs) and leave page shells for last.

  1. Install Tailwind and its Vite or PostCSS plugin; add @import "tailwindcss";.
  2. Move color, spacing, and radius values from the old CSS into @theme.
  3. Convert one component at a time. Keep the old stylesheet until that component is verified, then delete its rules.
  4. Do not mix old classes and utilities on one element. Old unlayered CSS beats Tailwind’s layered utilities, so the result depends on cascade order, not on what you wrote; see css-cascade-layers.

The coexistence window can last weeks. Aim for steady progress, not a flag day.

Migrate CSS Modules incrementally

Convert each component, then delete its .module.css. Where a module must stay, add @reference "../app.css"; to access the theme with @apply, or read tokens through var(--color-brand). Tailwind documents CSS Modules as workable but not recommended: each module is processed separately. Compare approaches in tailwind-vs-css-modules.

Settle conventions before the first PR

Agree on the Prettier plugin (commit the sort churn once), the token naming scheme (semantic or scale), and the extraction threshold (three uses). Record them in the repo so they survive turnover.