Tailwind v4: 'tailwindcss directly as a PostCSS plugin' error, fixed

[postcss] It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin. Every cause of this Tailwind v4 error, plus the silent one that hits next.

css tailwind-css tailwind-v4 postcss vite nextjs build-errors frontend
Bharath
Reading Progress

On This Page

[vite:css] [postcss] 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.
file: /tmp/tw/repro/style.css:undefined:NaN
    at mt (/tmp/tw/repro/node_modules/tailwindcss/dist/lib.js:38:1643)
    at LazyResult.runOnRoot (/tmp/tw/repro/node_modules/postcss/lib/lazy-result.js:361:16)

If that's your terminal, the one-line fix is: put @tailwindcss/postcss where tailwindcss is in your PostCSS config, and change your CSS entry to @import "tailwindcss";. Done, mostly.

"Mostly" is the reason this article is longer than that. I ran the fix six different ways today and two of them make the error go away while leaving you with a site that has no Tailwind on it. Nothing crashes. Nothing warns. That's the version that costs you the afternoon.

What the build prints

Same message, different wrappers. Depending on your stack you'll see it as:

  • [vite:css] [postcss] It looks like… in a Vite build, and as an HTTP 500 plus a red overlay in vite dev (I curled the stylesheet URL and got a 500 with the message inside the error page).
  • Module build failed (from ./node_modules/postcss-loader/dist/cjs.js): Error: It looks like… under webpack, which is how it shows up in the GitHub discussions.
  • The same sentence wrapped in whatever the Angular CLI or your Storybook builder puts around a PostCSS failure.

The tells that it's this exact problem and not some other PostCSS failure: the stack has tailwindcss/dist/lib.js in it, PostCSS's own LazyResult.runOnRoot right underneath, and the file: line points at a stylesheet with undefined:NaN for line and column. That last part means the plugin threw before it looked at your CSS at all. Your CSS isn't the problem yet.

It's tracked in a pile of places. The original report is tailwindlabs/tailwindcss#15735, and people were still hitting it on 4.1.2 (discussion #17535), plus a postcss-loader flavour in #16828 and an Angular library-build report at angular/angular#59784. I couldn't load the Stack Overflow hot-questions feed from my environment (blocked), so I fell back to searching by the error text, and the GitHub threads are where the volume is.

A Vite repro with the old config

No Tailwind knowledge needed for this, just a v3-style setup with v4 installed. I used Node 22, and pinned everything so you get the same output.

json

{
  "name": "repro",
  "private": true,
  "type": "module",
  "scripts": { "dev": "vite", "build": "vite build" },
  "devDependencies": {
    "vite": "7.1.5",
    "tailwindcss": "4.3.3",
    "postcss": "8.5.6",
    "autoprefixer": "10.4.21"
  }
}

js

// postcss.config.js  (ESM, because package.json says "type": "module")
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

css

/* style.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>repro</title>
</head>
<body>
  <h1 class="text-3xl font-bold underline text-blue-600">Hello</h1>
  <script type="module" src="/main.js"></script>
</body>
</html>

main.js is one line, import "./style.css". Then:

bash

npm install
npx vite build

You get the error from the top of the page. Now you have something to break on purpose.

One side trip that cost me ten minutes, because it's a real trap when you copy old configs: my first attempt used module.exports = {…} in postcss.config.js inside a "type": "module" package. Vite 7.1.5 didn't even reach Tailwind. It printed:

text

[vite:css] Failed to load PostCSS config (searchPath: /tmp/tw/repro): [ReferenceError] module is not defined in ES module scope

Different bug, same neighbourhood. If you see that one, rename the file to postcss.config.cjs or switch it to export default. The v3 docs used module.exports everywhere, so old configs trip this the moment the package goes ESM.

Who's affected

The error itself is not engine-specific, there's no browser in the loop, it's a build-time throw. What matters is the tool matrix.

ThingVersion I testedWhat happens
tailwindcss4.3.3 (npm latest today)Throws the message when PostCSS runs it as a plugin
tailwindcss3.4.x (v3-lts tag)Works as a plugin, no error
@tailwindcss/postcss4.3.3The v4 PostCSS plugin
@tailwindcss/vite4.3.3The Vite plugin, no PostCSS needed
@tailwindcss/cli4.3.3Owns the tailwindcss binary in v4

Vite 7.1.5 and postcss 8.5.6 for the build side. Frameworks: Next.js, Angular, Nuxt and Storybook show up in the reports, and in each of them it's the same root cause with a different config filename (postcss.config.mjs, .postcssrc.json, a Vite plugin list).

Browser side, since people ask: Tailwind's upgrade guide says v4 targets Safari 16.4+, Chrome 111+ and Firefox 128+. If you must support older ones, the honest answer is v3.4 (still published under the v3-lts dist-tag), not a config fix. Node 20+ for the upgrade tool.

What changes next: I found nothing suggesting the old plugin path is coming back in the v4 line. Check the release notes before assuming otherwise.

So what actually went wrong?

In v3, the tailwindcss package was a PostCSS plugin. You listed it in postcss.config.js and PostCSS called it. In v4, tailwindcss is the compiler core, and the PostCSS glue moved to @tailwindcss/postcss. If your config still says tailwindcss: {}, PostCSS loads the core package, calls it like a plugin, and the core package throws that sentence on purpose. It's a tombstone with directions on it.

That's the whole mechanical cause. The reason it survives in real repos is that "your config still says tailwindcss" has more places to hide than you'd think. I'll rank them in a minute.

The anecdote, since I promised myself one: a preview deploy once went red after a lockfile refresh nobody thought was risky, and it took a while to notice the pipeline installed tailwindcss unpinned in a package that nobody had touched in a year. The app code was fine. The dependency floated.

Why tailwindcss stopped being a PostCSS plugin

Look at what @tailwindcss/postcss depends on, straight from its package.json at 4.3.3: @tailwindcss/node, @tailwindcss/oxide, tailwindcss, postcss, and a small LRU cache. oxide is the Rust scanner that reads your source files for class names. So the PostCSS package isn't a thin re-export. It wires the compiler to PostCSS: source scanning happens there, and it hands the finished CSS back to the pipeline.

Tailwind's upgrade guide also says that in v4 imports and vendor prefixing are handled for you, so postcss-import and autoprefixer can go. I'll come back to autoprefixer.

The shift you need to absorb is that the entry file changed meaning. @tailwind base; @tailwind components; @tailwind utilities; were three insertion points. @import "tailwindcss"; is one line that pulls in a real stylesheet, node_modules/tailwindcss/index.css, which starts like this:

css

@layer theme, base, components, utilities;

@layer theme {
  @theme default {
    --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", …

That @theme default block is where --color-blue-600 and the rest of the default palette live. Keep that in your head, it explains both silent failures below.

The v4 binary moved too. The tailwindcss package in 4.3.3 has no bin at all. The tailwindcss command now comes from @tailwindcss/cli. If your package.json scripts call tailwindcss -i … -o … and you only installed the core package, you'll get a different error, and the fix is the same idea: install the package that owns the job.

Wrong turns, then the path that worked

Wrong turn one: a monorepo hoisting problem. The Angular and Nuxt reports made me suspect that a parent-directory postcss.config.js was leaking into a workspace package. So I built a mono/apps/web layout with the stale config at the repo root. Result on Vite 7.1.5: no error at all, and a 21 KB CSS file with no utilities in it (the same silent failure as below). Vite didn't pick up the parent config in my setup. So the theory didn't produce this error, and I dropped it. I only tested one layout, so if you have a workspace where a parent config does get picked up, I'd expect the same error, but I haven't seen it.

Wrong turn two: rename the plugin and call it done. I changed the config to @tailwindcss/postcss, left the CSS alone, and the build went green. The bundle was 0.11 KB. I checked the output:

/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */.underline{text-decoration-line:underline}

One class. .underline. My heading had text-3xl font-bold text-blue-600 on it and none of those were there. Green build, unstyled page. The legacy @tailwind utilities; directive still runs in v4, but it doesn't bring the default theme with it, so any utility that needs a theme variable (text-blue-600 wants --color-blue-600) has nothing to resolve against and gets dropped. underline needs no theme value, so it survives. I'm inferring the mechanism from the output and the structure of index.css, not from reading the compiler, but the behaviour is exactly what I measured.

The tell. After any Tailwind v4 change, open the built CSS and search for a utility you know you used. Not "does the build pass". Search the output for text-blue-600, or check the file size. 0.11 KB or a file with no utilities means the pipeline ran and produced nothing. That's how I told the causes apart.

Here's the path that works on your own repo, in order:

  1. Read the exact message. Does it say [postcss] It looks like you're trying to use tailwindcss directly…? Then some PostCSS config still names tailwindcss. Next step.
  2. Find every config PostCSS can read. It isn't only postcss.config.js. Run this from the project root:

bash

   ls -a | grep -Ei 'postcss|tailwind|vite.config|next.config'
   grep -n '"postcss"' package.json

Look for postcss.config.{js,cjs,mjs,ts}, .postcssrc*, and a "postcss" key inside package.json. I tested the package.json key: it produces the identical error. 3. Check whether you're on the Vite plugin. If vite.config already has @tailwindcss/vite, you don't want a PostCSS Tailwind entry at all. I tested @tailwindcss/vite with the stale PostCSS config still in place: same error. With the PostCSS config deleted: clean 4.75 KB build. 4. Check the CSS entry. Any @tailwind base|components|utilities left over means you're in silent-failure territory even after the error is gone. 5. Check what actually resolved. npm ls tailwindcss @tailwindcss/postcss @tailwindcss/vite. All three should say the same 4.x number. A v3 copy hoisted somewhere else is its own headache. 6. Verify the output, not the exit code. Build, then grep -c text-blue-600 dist/assets/*.css (swap in a class you use). Zero hits means step 4 or step 3 is still broken.

If you're in a browser and the page is unstyled but the build passed, DevTools tells you which world you're in. In the Network panel, open the CSS response and look at the top. If you see @layer theme,base,components,utilities;@layer theme{@theme default{ or a literal @tailwind utilities; in the file, Tailwind never ran on it. Browsers ignore at-rules they don't know, so the page renders unstyled and the Console says nothing. I confirmed both strings in a production bundle when I deleted the PostCSS config without adding the Vite plugin.

Ranked by how often I'd expect it to be the cause (my judgment from the reports and my six runs, not a measurement): a stale tailwindcss: {} in postcss.config.* first, then a postcss.config left behind after switching to @tailwindcss/vite, then old @tailwind directives, then a config in a format nobody remembers (.postcssrc.json, a package.json key). In my repro it was the first one.

The fix, per bundler

Vite, the option I'd ship:

bash

npm uninstall autoprefixer
npm i -D tailwindcss @tailwindcss/vite
rm postcss.config.js   # only if Tailwind was the only thing in it

diff

 // vite.config.js
 import { defineConfig } from 'vite'
+import tailwindcss from '@tailwindcss/vite'

 export default defineConfig({
-  plugins: [],
+  plugins: [tailwindcss()],
 })

diff

 /* style.css */
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@import "tailwindcss";

Delete the PostCSS config or you'll keep the error (I tested that). And don't delete it and forget the plugin, or you get the 21 KB unprocessed file.

PostCSS route (Next.js, webpack, Angular, anything else):

bash

npm i -D tailwindcss @tailwindcss/postcss postcss

js

// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
}

That's the shape from Tailwind's own PostCSS and Next.js guides. For Angular, their guide uses .postcssrc.json:

json

{
  "plugins": {
    "@tailwindcss/postcss": {}
  }
}

The automated route: npx @tailwindcss/upgrade (Node 20+). I ran it on a v3.4.17 Vite project. It rewrote the CSS entry to @import 'tailwindcss';, bumped tailwindcss, installed @tailwindcss/postcss, and updated my postcss.config.cjs in place (kept the .cjs extension). It removed the empty tailwind.config.js, and added a compatibility block that sets border-color back to gray-200 because v4's default border colour changed to currentcolor. It left autoprefixer in the config, harmless but redundant. The resulting build had the utilities. Review the diff and commit it. I like the tool and I don't fully trust any codemod, so read what it did.

Things that make the error vanish without fixing anything: pinning back to tailwindcss@3 and forgetting why, running installs with --force until the resolver stops complaining, and copying a snippet that renames the plugin but leaves @tailwind directives in place, which is my second wrong turn. All of them give you a green build. Only the first is a decision you'd defend in code review.

What I'd ship: one plugin, one import, no autoprefixer

The version I'd put in a starter template, on Vite:

js

// vite.config.js
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [tailwindcss()],
})

css

/* src/style.css */
@import "tailwindcss";

@theme {
  --color-brand: oklch(0.62 0.19 259);
}

No postcss.config.*, no tailwind.config.js, no autoprefixer. One less file that can go stale. Use the PostCSS plugin only when your framework owns the bundler and PostCSS is the integration point (Next.js, Angular), and in that case keep exactly one config file, one format, in the app directory.

My opinion, with a reason: pin the minor of tailwindcss and its companion packages together, using an exact version or a range you've actually reviewed. All three packages ship at the same version number (4.3.3 today) and the plugin depends on the exact same version of the core. Mixing them is how you get errors that don't say what's wrong.

Guardrails for the next major bump

The bug class is "config for tool version N, tool version N+1 installed, build passes anyway". Catch that.

  • Assert on the output. A CI step that builds and then greps the emitted CSS for one utility you know you use. It takes five lines of shell and would have caught both silent failures above:

bash

  npm run build
  grep -q 'text-blue-600' dist/assets/*.css || { echo "Tailwind output missing"; exit 1; }
  • A size floor. If your CSS bundle is normally 40 KB and one PR makes it 0.11 KB, fail the job.
  • One lockfile, npm ci in CI. Not npm install. The lockfile is what stopped my floating-version problem from recurring.
  • Renovate or Dependabot with grouping for tailwindcss, @tailwindcss/* and postcss, so they move together and a major bump shows up as a PR you can look at.
  • A test that opens the built page in Chromium, Firefox and WebKit (Playwright projects) and checks a computed style, for example getComputedStyle(h1).fontSize against what text-3xl should give. It catches the unstyled-but-green build on every engine.
  • Fail on console and network errors in that test, because a CSS chunk 404 is the same silent class of problem.
  • One PostCSS config, in a documented place. Say in the README whether you use the Vite plugin or the PostCSS plugin.

I'd skip a stylelint rule for this. Nothing in stylelint knows about your package versions.

Worth pinning

  1. The error means a PostCSS config still names tailwindcss. Change it to @tailwindcss/postcss, or delete PostCSS and use @tailwindcss/vite.
  2. Green build is not success. Search the built CSS for a class you used.
  3. @tailwind utilities; alone builds fine in v4 and drops anything that needs theme values. @import "tailwindcss"; is the entry now.
  4. PostCSS config can live in a file, a .postcssrc, or a package.json key. Check all three.
  5. Install the packages that own the job: @tailwindcss/postcss for PostCSS, @tailwindcss/vite for Vite, @tailwindcss/cli for the command line.

Neighbouring topic, for later interlinking: this is the toolchain twin of "Tailwind classes not applied" (@source and content detection). Once the build is fixed, that's where a missing class usually goes next.

csstailwind-csstailwind-v4postcssvitenextjsbuild-errorsfrontend

Engineer and writer behind CODELZ. I read the stack trace, reproduce the bug, and explain why it happens, not just how to silence it. Plus honest reviews of the books and tools worth your time.

Comments