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/upgradeThe 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-smtoshadow-xs,shadowtoshadow-sm, the same shift forblur,drop-shadow,backdrop-blur, androunded;outline-nonetooutline-hidden;ringtoring-3. - Removed utilities:
bg-opacity-*and its siblings becomebg-black/50;flex-shrink-*andflex-grow-*becomeshrink-*andgrow-*;overflow-ellipsisbecomestext-ellipsis. - Defaults: the default border color is
currentColorandringis 1px, so set border colors explicitly. Buttons usecursor: default, andhover: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:
containeroptions are gone (extend it with@utility container);safelistbecomes@source inline(); a JS config loads only via@config, withoutcorePlugins,safelist, orseparator. 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.
- Install Tailwind and its Vite or PostCSS plugin; add
@import "tailwindcss";. - Move color, spacing, and radius values from the old CSS into
@theme. - Convert one component at a time. Keep the old stylesheet until that component is verified, then delete its rules.
- 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.