Tailwind v4 in Next.js: Fixing 'trying to use tailwindcss directly as a PostCSS plugin'
You upgraded Tailwind, ran npm run dev, and Next.js refused to compile before it rendered a single pixel. The stack trace points at your CSS, not your code, which is the kind of error that makes you doubt the last three commands you ran. Nothing in your components changed. The build just stopped trusting tailwindcss.
This is the single most common wall people hit moving to Tailwind CSS v4, and it is not a bug in your project. In v4 the PostCSS plugin was pulled out of the main tailwindcss package into its own package, @tailwindcss/postcss. Every v3 tutorial, boilerplate and Stack Overflow answer that tells you to put tailwindcss: {} in your PostCSS config is now wrong, and the error you are reading is Tailwind telling you exactly that.
The fix is three small edits. Understanding why it happened will save you from the second and third errors waiting behind this one, because the v4 migration changes more than one file.
In Tailwind v4 the PostCSS plugin lives in a separate package. Install it and point your config at it.
- Run
npm install -D @tailwindcss/postcss - In
postcss.config.mjs, replacetailwindcss: {}with'@tailwindcss/postcss': {}and delete theautoprefixerline (v4 handles it). - In your global CSS, replace the three
@tailwinddirectives with a single@import "tailwindcss"; - If it still fails, you have both v3 and v4 installed at once. Run
npm ls tailwindcssand remove the stray version.
The exact error: "trying to use tailwindcss directly as a PostCSS plugin"
The full message, printed the moment Next.js tries to process your CSS, is:
It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install `@tailwindcss/postcss` and update your PostCSS configuration.
Sometimes Next.js wraps it in a longer trace mentioning Error: Cannot apply unknown utility class or a failed module further down. Ignore the noise. If the phrase "moved to a separate package" appears anywhere in the output, this is your problem and nothing else is.
Why this happens: the plugin is no longer the package
In Tailwind v3, the tailwindcss npm package did two jobs at once. It was the engine that generated your utility classes, and it also exposed a PostCSS plugin interface so that PostCSS could call it. That is why the v3 config looked like this:
// postcss.config.js (Tailwind v3 - do not use this in v4)
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
When PostCSS saw tailwindcss: {}, it imported the tailwindcss package and expected to find a PostCSS plugin sitting there. In v4 that plugin is gone from the main package. The Tailwind team rebuilt the engine on a new high-performance core (much of it in Rust) and split responsibilities so the framework integrations each live in their own package: @tailwindcss/postcss for PostCSS-based builds like Next.js, @tailwindcss/vite for Vite, and a standalone CLI.
So when your v3-shaped config asks PostCSS to load tailwindcss as a plugin, PostCSS finds the engine but no plugin interface. Tailwind detects that misuse and, instead of a cryptic type error, prints the friendly message telling you the plugin moved. It is one of the better-designed error messages in the ecosystem. The trouble is that thousands of published configs still show the old shape, so a fresh developer copies a two-year-old postcss.config.js and walks straight into it.
The fix, step by step for Next.js App Router
Step 1: Install the PostCSS package
From your project root:
npm install -D @tailwindcss/postcss
You do not need to uninstall tailwindcss. You still need the engine; you are just adding the plugin bridge next to it. Make sure tailwindcss itself is on v4:
npm ls tailwindcss
Expected output shows a single 4.x entry, for example tailwindcss@4.1.11. If you see a 3.x version, upgrade it with npm install -D tailwindcss@latest.
Step 2: Rewrite the PostCSS config
Next.js reads postcss.config.mjs (or .js) from the project root. Replace its contents so the plugin key is the new package and drop autoprefixer:
// postcss.config.mjs
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
Two things people miss here. First, the plugin key is a quoted string because it contains a slash and an @. Second, you no longer list autoprefixer. Tailwind v4 runs vendor prefixing and nesting internally, so keeping autoprefixer in the list is at best redundant and at worst a source of double-prefixed CSS. Remove it and uninstall it if nothing else uses it.
Step 3: Switch the CSS entry file to a single import
In v3 your globals.css started with three directives:
/* v3 - remove these */
@tailwind base;
@tailwind components;
@tailwind utilities;
In v4 those become one line:
/* app/globals.css - v4 */
@import "tailwindcss";
If you leave the old @tailwind directives in place you will get a separate error about unknown at-rules once the plugin loads, so change this in the same pass rather than waiting for it to bite you.
Step 4: Move your config into CSS (the part that surprises people)
This is where the migration goes from a one-line fix to a real change. Tailwind v4 is CSS-first. Your theme customisations, custom colours, fonts and breakpoints no longer live in tailwind.config.js. They live in your CSS inside an @theme block:
/* app/globals.css */
@import "tailwindcss";
@theme {
--color-brand: #1f7a4d;
--font-display: "Inter", sans-serif;
--breakpoint-3xl: 120rem;
}
That --color-brand line makes bg-brand, text-brand and friends real utility classes. You can still keep a JavaScript config for backwards compatibility by loading it explicitly with @config "../tailwind.config.js";, and that is a reasonable bridge for a large project mid-migration. But new projects should treat CSS as the source of truth. If you upgraded and half your custom colours vanished, this is why: they are still sitting in a tailwind.config.js that v4 no longer reads automatically.
Verification: prove the build is clean
Restart the dev server from a clean state so you are not reading a cached error:
rm -rf .next
npm run dev
A healthy start compiles with no CSS error and serves the page with styles applied. To confirm utilities are actually being generated rather than silently dropped, run a production build:
npm run build
Expected: the build completes, and a quick grep of the emitted CSS shows your classes. If a custom utility like bg-brand is missing from the output, the class is defined but your @theme token is not, which points you back to Step 4 rather than the PostCSS config.
What people get wrong
Downgrading back to v3 to "make the error go away." This works, and it is the wrong move if you started the upgrade on purpose. You will hit v4 again the next time you scaffold a project or pull a dependency that expects it. Spend the twenty minutes now.
Adding @tailwindcss/postcss but leaving tailwindcss: {} in the config too. Listing both plugins does not help. PostCSS still tries to load the old plugin entry point and still throws. There should be exactly one Tailwind entry in your plugins object, and it must be the new package.
Assuming the error means Tailwind is not installed. The message mentions installing a package, so people run npm install tailwindcss again and are baffled when nothing changes. The engine was never missing. Read the sentence to the end: it asks for @tailwindcss/postcss, a different package.
When it is still broken: the version-mismatch trap
If you did all four steps and the identical error still appears, you almost certainly have two versions of Tailwind installed at once, usually because a dependency or a starter template pins v3 transitively. The v4 plugin refuses to run against a v3 engine and prints the same message.
Diagnose it:
npm ls tailwindcss
If the tree shows both a 3.x and a 4.x entry, find what pulls in the old one, deduplicate, and delete the lockfile and node_modules if the tree is tangled:
rm -rf node_modules package-lock.json
npm install
In a monorepo the mismatch is often that the root and a workspace each declare a different major version. If you keep seeing dependency resolution surprises across a workspace, the same root cause shows up elsewhere; I wrote about one flavour of it in Next.js 'Found multiple lockfiles'. And if your build fails for reasons that turn out to be memory rather than config, JavaScript Heap Out of Memory on next build covers that path.
For the canonical details on the theme system and the @config escape hatch, the official Tailwind v4 upgrade guide is worth ten minutes once the build is green again.
Frequently asked questions
- Do I have to uninstall tailwindcss when I install @tailwindcss/postcss?
- No. The tailwindcss package is still the engine that generates your utility classes and you need it. You only add @tailwindcss/postcss alongside it as the PostCSS bridge, then point your postcss config at the new package instead of at tailwindcss directly.
- Where did tailwind.config.js go in Tailwind v4?
- Tailwind v4 is CSS-first, so theme values now live in an @theme block inside your CSS rather than in tailwind.config.js. You can still load a JavaScript config explicitly with @config "./tailwind.config.js"; as a migration bridge, but v4 no longer reads it automatically, which is why custom colours often disappear after upgrading.
- Why do I still get the error after installing @tailwindcss/postcss?
- Almost always because two versions of tailwindcss are installed at once, usually a v3 pulled in transitively by a template or dependency. Run npm ls tailwindcss; if you see both a 3.x and a 4.x, deduplicate, and if needed delete node_modules and the lockfile and reinstall.
- Do I still need autoprefixer with Tailwind v4?
- No. Tailwind v4 handles vendor prefixing and CSS nesting internally, so you should remove the autoprefixer entry from your PostCSS plugins and can uninstall it if nothing else uses it. Leaving it in can produce redundant double-prefixed output.