Overview

Migrations succeed when they are incremental and tested. Rewriting a codebase to Tailwind in a single PR is the failure mode; the team cannot review it, the diff is enormous, and subtle regressions hide in the noise. The parent reference is tailwind; for the full Tailwind v3 vs v4 diff, the v4 upgrade guide is the primary source.

Migrate from custom CSS leaf-first

Start with the smallest leaf components: buttons, badges, input fields. Leave the page shells and layout containers to the end.

  1. Install Tailwind v4 and its Vite plugin (or CLI for non-Vite stacks).
  2. Import tailwindcss in your entry CSS file.
  3. Extract color, spacing, and radius tokens from the existing CSS into @theme. See tailwind-theme for the token extraction pattern.
  4. Port one leaf component at a time. Keep the original CSS file until the component is fully converted; do not mix Tailwind classes and the old CSS on the same element.
  5. Delete the old CSS file for each component once the conversion is verified in the browser.

The coexistence window, where Tailwind and custom CSS run side by side, can last weeks or months. That is fine. The goal is forward momentum, not a flag day cutover.

Migrate from Tailwind v3 to v4

v4 removes the JavaScript config file and changes several APIs. Run the official codemod first.

npx @tailwindcss/upgrade

The codemod handles:

  • Moving tailwind.config.js theme values into @theme in your CSS entry file.
  • Replacing deprecated theme() function calls with var(--token-name).
  • Updating import syntax from @tailwind base directives to @import "tailwindcss".
  • Renaming changed utility classes (e.g., shadow-sm defaults changed; ring offset changes).

After running the codemod, rebuild the project and review the diff manually. The codemod handles the mechanical changes; semantic changes, like updated default values for shadows and ring widths, require a visual review.

Audit and port the JavaScript config manually

The codemod covers most theme.extend values but may miss complex patterns:

  • theme.extend.colors with function callbacks that read other tokens.
  • plugins: entries using the v3 plugin API. Each becomes either a first-party @plugin or a custom @layer utilities block; see tailwind-plugins.
  • safelist: arrays. Move these to explicit uses in templates or to @layer utilities custom classes.
  • content: array entries. v4 auto-detects content; remove the array.

Compare the old tailwind.config.js against the generated @theme block after the codemod runs. Any key in the config that does not appear in @theme is a gap to fill manually.

Extract component classes instead of using @apply

When migrating existing CSS class names to Tailwind, resist the temptation to rewrite them as @apply blocks.

/* Avoid: hides the implementation behind a name */
.btn-primary {
  @apply bg-brand text-white px-4 py-2 rounded-md font-medium;
}
 
/* Prefer: inline utilities or a real component */

The exception is third-party or CMS-rendered markup you cannot annotate with classes. Use @apply there, but document why, and scope it to the specific element, not a general class.

Set team conventions before the migration starts

A migration is a forcing function to adopt conventions the team should have had anyway. Agree on these before the first PR:

  • Prettier plugin for class sorting: install it, commit the sort churn, enforce it in CI.
  • Token naming: agree on whether to use semantic names (--color-surface) or scale names (--color-neutral-100) or both.
  • Component extraction threshold: agree on the three-uses rule from the start.
  • @apply policy: agree it is banned except for the documented exceptions.

Write these conventions in a short ADR or in a project-level CLAUDE.md so they survive team turnover.

Migrate CSS Modules to Tailwind incrementally

When migrating a CSS Modules codebase, port components leaf-first and keep .module.css files for components you have not yet ported.

Per component:

  1. Read the .module.css file and identify all values that should become design tokens.
  2. Add missing tokens to @theme.
  3. Rewrite the component’s class names to Tailwind utilities.
  4. Delete the .module.css file.
  5. Run node scripts/lint-content.mjs and the build to confirm no regressions.

Do not add Tailwind classes alongside CSS Module classes on the same element. The specificity interaction is unpredictable and the mixed styles are hard to review.