If your package.json still lists tailwindcss at version 3, the first move is small: create a throwaway branch, run npx @tailwindcss/upgrade, and read the diff before you decide anything else. This site is in that spot right now, with tailwindcss ^3.4.18 declared in its own package.json, so what follows is the plan I'd run on it, not a changelog summary. It suits the stack I recommend in the SaaS MVP stack guide: Next.js, TypeScript and the utility-first CSS approach Tailwind takes.
The stakes depend on what your CSS carries. Take a site like Pizzeria Bestek, which the case study puts at five to ten orders a day: a broken utility on the checkout page costs orders, which makes a styling change a business risk as well as a visual one. That is the reason to migrate in steps you can revert.
What Breaks When You Run the Tailwind v4 Upgrade Tool?
The tool rewrites what it can read statically, so the breakage sits in whatever it cannot see: class names built at runtime, options v4 dropped, and settings whose defaults changed. Tailwind's upgrade guide says the tool needs Node.js 20 or higher and is run with npx @tailwindcss/upgrade.
Dynamic class names and safelists
Tailwind scans your source as plain text. The official docs on detecting classes warn that it "has no way of understanding string concatenation or interpolation", so a class like bg-${color}-600 is never generated, in v3 or v4. If your components assemble class names from props, map each prop value to a complete class string before you migrate. The safelist option is a separate case: the v4 docs state that corePlugins, safelist and separator from a JavaScript config are not supported in v4.0, and the replacement is @source inline() in your CSS.
The PostCSS plugin rename
In v3, postcss.config.js lists tailwindcss as a plugin. In v4, the PostCSS plugin lives in a dedicated @tailwindcss/postcss package, and imports and vendor prefixing are handled automatically, so postcss-import and autoprefixer can go. In a monorepo this is worth a search: a shared package whose PostCSS config still names tailwindcss will resolve a different entry point than your app does. That is my inference from how the rename works, so grep every postcss.config.* in the repo before you merge.
Renamed utilities and changed defaults
The renames are find-and-replace work. The changed defaults are the ones that fail quietly, because nothing throws an error. The table below lists the ones the guide documents.
| Tailwind v3 | Tailwind v4 | |
|---|---|---|
| Config | tailwind.config.js, detected automatically | @theme in CSS; a JS config loads only through @config |
| PostCSS plugin | tailwindcss | @tailwindcss/postcss |
flex-shrink-* / flex-grow-* | Valid | shrink-* / grow-* |
shadow-sm / shadow | Small and default shadows | Renamed to shadow-xs / shadow-sm |
| Opacity | bg-opacity-50 | bg-black/50 |
| Default border color | gray-200 | currentColor |
| Gradient with a variant override | Resets the whole gradient | Other stops are preserved |
| Custom utilities | @layer utilities | @utility |
The border row is the one to test by eye. The guide says that border-* and divide-* used your gray-200 by default in v3 and now use currentColor, so any border without an explicit color changes appearance. The theme() function is also deprecated in favor of CSS variables, per the directives reference, which matters if your custom CSS calls it.
You don't have to move everything at once
Two directives let you migrate in stages. The directives reference says @config loads a legacy JavaScript config and @plugin loads a legacy JavaScript plugin, both kept for compatibility, and that they can be combined with @theme and @utility to move a project over incrementally. That gives you a sensible order: get the build green with @config pointing at your old file, then retire that file section by section, starting with theme tokens and ending with plugins. Each step is a small commit, so a bad one is easy to revert.
What to look for when you read the first diff
Read the raw output of PR one as a reviewer would, with three questions in mind. Did it touch every template folder you expect, including shared UI packages and Storybook or documentation apps? Did it leave any file with a class name it could not parse, which you will see as an unchanged flex-shrink-0 next to a renamed one? And did it change your lockfile in a way that pulls a second copy of Tailwind into a workspace? None of these has an automatic answer, and each one turns into a bug report if you skip it.
How Do You Run the Tailwind v3 to v4 Migration in Two Pull Requests?
Keep the mechanical output and the manual fixes in separate pull requests, so the first proves the tool's changes build and the second holds every decision that needs a human. This is my recommended process, not something the docs prescribe.
- PR one: the raw upgrade. Branch off
main, runnpx @tailwindcss/upgrade, and commit the output without hand edits. Let CI run. - PR two: the manual fixes. Search for
theme(,safelist, string-built class names and@layer utilities. Move custom utilities to@utility, and add explicit colors wherever you relied on the old border default. - Look at the pages. Open your highest-traffic pages in a real browser, in light and dark mode and at phone width. A DOM snapshot test cannot see a color that changed.
- Deploy to a staging URL first. A wrong utility fails every page that uses it at once, which is why a preview deploy is cheap insurance.
- Remove leftovers last. Delete the old
postcss-importandautoprefixerentries after the new build has been live for a while, since keeping them briefly makes a revert easier.
Where the typography plugin fits
The plugin is compatible with both majors. The npm registry lists @tailwindcss/typography 0.5.20 with a peer range of tailwindcss >=3.0.0 || >=4.0.0, and the v4 docs load it from CSS with @plugin "@tailwindcss/typography";. If your tailwind.config.js still has it in a plugins array, move that line into your stylesheet and load the config file explicitly, or migrate the settings into CSS.
Make CI catch what the tool cannot
Add two cheap checks to the branch before you merge either pull request. First, run a production build, not only the dev server, because a missing class or a bad PostCSS entry can pass in development and fail in the pipeline. Second, write down the pages that define your product, such as pricing, sign-up, checkout and the main dashboard view, and open each one at desktop and phone width in both color schemes. A short written list turns "looks fine" into something a teammate can repeat. If you have end-to-end tests, run them against the staging deploy and not only against a local server, since the deployed build is the artifact your users will receive. None of this is specific to Tailwind, but a stylesheet change touches every page at once, which makes these checks worth more here than for a typical feature branch.
Is Tailwind v4 Safe for Production in 2026?
Yes for most projects, if your audience is on Safari 16.4, Chrome 111 or Firefox 128 and newer. The upgrade guide names those as the browsers v4 is designed for, because it relies on @property and color-mix().
Check your analytics for older versions before you pick a date. If a meaningful share of visitors is below those floors, staying on v3 is a legitimate choice, and it is still maintained: the npm registry has a v3-lts tag pointing at 3.4.19, published December 2025, while latest is 4.3.3, published on 16 July 2026.
What the speed numbers are
Tailwind's v4.0 announcement of 22 January 2025 reports Catalyst benchmarks: full builds dropped from 378ms to 100ms, and incremental rebuilds with no new CSS from 35ms to 192 microseconds. Those are Tailwind's numbers on their own project, so measure your build before and after and don't promise your team a figure.
Config as CSS
The old tailwind.config.js was a settings file. The @theme block makes your tokens CSS variables you can read at runtime, which is closer to a working design system than a build-time object. That is the part worth wanting once the migration itself is out of the way.
When staying on v3 is the right call
A migration is a cost, and a project with a small audience on older browsers, a plugin you cannot replace, or a release freeze has good reasons to wait. Staying is a decision you should write down with a date to revisit it, so it does not turn into an accident. The practical test is simple: if the upgrade tool's diff on a throwaway branch is small and your analytics clear the browser floor, migrate this quarter. If either fails, note which one, and check again when your constraint changes.
What to Do This Week
Run the upgrade tool on a throwaway branch and count the files it changes. Then search your repo for safelist, theme( and string-built class names, because that list is your second pull request. If you find a category of breakage I haven't listed, tell me what it was.



