Tailwind v4: 'trying to use tailwindcss directly as a PostCSS plugin'

Your Tailwind v4 build dies with 'It looks like you're trying to use tailwindcss directly as a PostCSS plugin': here's every real cause and the actual fix.

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

On This Page

What the terminal prints instead of a running dev server

[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.

That's the whole message. No file name, no line number, no stack trace pointing at your code. Just a wall of text that stops npm run dev cold and hands you a link to read documentation you probably already read once.

If you're on Vite the wording is slightly different but means the same thing: Failed to load PostCSS config followed by the same paragraph, or occasionally [vite:css] Preprocessor dependency "tailwindcss" not found, depending on which half of your config is stale. Next.js wraps it in its own red overlay but the inner message is identical, because it's PostCSS talking, not the framework.

Here's the annoying part: your postcss.config.mjs almost certainly looks fine to your eyes. You installed Tailwind. You ran the install command from a tutorial, or a boilerplate, or, increasingly in 2026, an AI coding assistant that scaffolded the project for you. Everything looks like it should work. It doesn't, and the error message, while technically accurate, doesn't tell you which of six possible reasons is yours.

One file, one error, no server

Here's the smallest project that reproduces it. No framework, just Vite and PostCSS talking to each other badly.

bash

npm create vite@latest repro -- --template vanilla-ts
cd repro
npm install
npm install tailwindcss postcss

Notice what's missing from that last line: @tailwindcss/postcss. That's the whole bug, but let's build it out so it actually fails the way yours did.

json

{
  "devDependencies": {
    "postcss": "8.5.28",
    "tailwindcss": "4.3.3",
    "vite": "8.3.0"
  }
}

javascript

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

css

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

html

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="/src/style.css" />
  </head>
  <body>
    <h1 class="text-3xl font-bold underline">Hello world</h1>
  </body>
</html>

Run npm run dev and it dies on first request with the exact message from section one. The CSS import syntax is right. The class names are right. The version installed is genuinely 4.3.3. The only thing wrong is one line in postcss.config.mjs, and that line is the one everyone copies from a search result written for Tailwind v3.

Which Tailwind version, which Node, which bundler

This isn't a browser bug, so skip the Blink/Gecko/WebKit table you'd expect from a rendering article. The matrix that matters here is tooling.

SetupFails with bare tailwindcss in PostCSS config?Fix
Tailwind v4.x + PostCSS (any framework)Yes, alwaysInstall and reference @tailwindcss/postcss
Tailwind v4.x + ViteOnly if you're using PostCSS instead of the Vite pluginUse @tailwindcss/vite and drop PostCSS entirely, or fix the PostCSS config
Tailwind v4.x + Next.js (App or Pages Router, webpack or Turbopack)Yes@tailwindcss/postcss in postcss.config.mjs, same fix either router
Tailwind v3.x anywhereNo — v3's plugin is still the bare tailwindcss packageNothing to fix, this article isn't about you
Mixed v3/v4 in the dependency graph (common in monorepos and AI-generated diffs)Yes, plus confusing secondary errors about @theme or @applyPin one major version everywhere, see the hunt below

Tailwind v4 itself needs Node 20 or newer for its build tooling, and the CSS it generates leans on cascade layers, @property, and modern color functions, which means the output needs Safari 16.4+, Chrome 111+, and Firefox 128+ to render correctly. None of that causes this particular error. I'm listing it because if you're mid-upgrade, it's the other thing that'll bite you five minutes after this one, and you don't want a second incident report the same afternoon.

One version note worth being specific about, because vague version claims are how these articles get out of date fast: at the time of writing, tailwindcss and @tailwindcss/postcss are both at 4.3.3, and they're released in lockstep, so mismatched minor versions between the two packages aren't the cause here, but they're worth ruling out with npm ls regardless.

So why won't tailwindcss run as a PostCSS plugin?

Because as of Tailwind v4, it isn't one. The core tailwindcss package stopped exporting a PostCSS plugin and became the engine underneath several different entry points instead: @tailwindcss/postcss for PostCSS pipelines, @tailwindcss/vite for Vite, and the standalone CLI for everyone else. When your postcss.config.mjs still hands PostCSS the bare tailwindcss package, PostCSS calls it expecting a plugin function and gets back something that isn't one. Tailwind's own code detects exactly that shape of mistake and prints the message you're staring at, on purpose, instead of failing with a cryptic undefined is not a function.

It's a good error message, as these things go. It just answers a question you didn't ask ("what's a PostCSS plugin supposed to look like") instead of the one you did ("why did my project stop building today").

How PostCSS decides what counts as a plugin

PostCSS's contract is narrow: a plugin is an object with a postcssPlugin string and either a Once or per-node visitor function, or a factory that returns one. When your config does { plugins: { tailwindcss: {} } }, PostCSS resolves the string "tailwindcss" to whatever require("tailwindcss") or its ESM equivalent returns, then calls it as a plugin factory.

Here's the part the error message glosses over: in v3, tailwindcss's main export genuinely was that factory. In v4, the main export is the compiler core (the thing that actually parses your @theme, resolves your utility classes, and generates CSS), with no PostCSS wrapper attached, because that wrapper now lives in a separate, smaller package that just adapts the core to PostCSS's plugin shape. Tailwind's team split it deliberately: v4 also ships a Vite-native adaptor and a Lightning CSS-based CLI that don't go through PostCSS at all, so bundling PostCSS glue into the main package would mean everyone pays for a dependency they might not use.

Think of it like a phone number that still rings, but at the old office instead of the new one. The reference is correct-looking. The thing it used to point at moved.

The reason this is worth understanding instead of just pattern-matching the fix: the same split explains why @tailwindcss/vite skips PostCSS's node-by-node CSS parsing and processing pipeline and talks to Tailwind's Rust-accelerated core directly, which is also why Vite builds with the native plugin are measurably faster than the PostCSS path for large stylesheets. If you're choosing your setup from scratch rather than fixing an existing one, that's the tiebreaker.

Chasing it through node_modules

I got bit by this rebuilding a client's marketing site. I'd asked a coding assistant to scaffold the project (new repo, Vite, Tailwind, the works) and it cheerfully wrote a postcss.config.js that could have come straight out of a 2024 blog post: postcss-import, tailwindcss, autoprefixer, in that order, module.exports and everything. Meanwhile package.json had already pulled down tailwindcss@4.3.3, because that's what npm install tailwindcss gives you today. Old config, new package. Neither half was wrong on its own.

First theory: Node version. I'd just switched machines and wasn't sure which Node the new one defaulted to, and v4's docs mention Node 20 as a floor. node -v came back 22.22.2. Ruled out in ten seconds, which in hindsight should have told me the fix was going to be smaller than the theory.

Second theory: I'd migrated the CSS import wrong. Tailwind v4 replaces the old @tailwind base; @tailwind components; @tailwind utilities; trio with a single @import "tailwindcss";, and I half-remembered leaving one of the old @tailwind lines in a partial. I opened the file. It was already the new single-line import, clean. Also not it.

The tell was npm ls tailwindcss. Run it whenever this error shows up, before anything else:

bash

npm ls tailwindcss --all
repro@0.0.0
└─┬ (project root)
  └── tailwindcss@4.3.3

One version, one copy. In my case the dependency graph wasn't the problem, which narrowed it straight down to the config file itself. If yours prints two different version numbers at two different depths, that's a different flavor of the same bug: something in your tree (a shared internal package, a copy-pasted package.json from an older project, a lockfile that survived the upgrade) is still pinned to tailwindcss@3.x, and PostCSS may be resolving whichever copy your bundler's module resolution finds first, not the one you expect. This shows up most in pnpm workspaces and Yarn monorepos, where a symlinked structure means "the" tailwindcss package your app sees isn't always the top-level one in your package.json.

A second check, when the first one comes back clean: read postcss.config.mjs (or .js, or the postcss block inside package.json; PostCSS will happily use any of the three, which is its own small trap) and grep for the literal string tailwindcss without the @tailwindcss/ prefix:

bash

grep -rn '"tailwindcss"\|tailwindcss:' postcss.config.* 2>/dev/null

If that prints a line where tailwindcss appears alone as a plugin key, you've found it. That was mine. Ranked by how often each cause is actually the one:

  1. Bare tailwindcss left in the PostCSS plugin list after an upgrade or a copy-pasted config: the large majority of reports, including mine.
  2. @tailwindcss/postcss never installed at all: same symptom, simpler cause, common when someone follows only half a tutorial.
  3. Mixed major versions somewhere in the dependency tree, usually in a monorepo or a workspace with a shared UI package still on v3.
  4. A stale node_modules/lockfile after an upgrade, where npm resolved and cached the old plugin shape before you edited the config.

Fixing the config, not just the symptom

The fix is the same one line whichever cause got you here, but do the version check first so you're not fixing the config while the real problem is a stray v3 package two levels down.

diff

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

bash

npm install -D @tailwindcss/postcss

Drop autoprefixer and postcss-import from the plugin list too, if they were there. Tailwind v4 does vendor prefixing and @import resolution internally, and leaving the old plugins in place is a second, quieter reason the same file can still misbehave even after the main error goes away.

If you're on Vite, you have a genuinely better option than "fix the PostCSS config": skip PostCSS for Tailwind entirely.

bash

npm install -D @tailwindcss/vite

typescript

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

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

That's what I'd actually ship for a Vite project. It's one less config file to keep in sync, and it's the path Tailwind's own docs lead with for Vite users.

If your tree turned up mixed versions (cause 3), the one-line diff above won't hold, so reinstall clean instead:

bash

rm -rf node_modules package-lock.json
npm install tailwindcss@4 @tailwindcss/postcss postcss
npm install

I don't love reaching for rm -rf node_modules as a fix; it's the CSS-toolchain equivalent of turning it off and on again, and it papers over the fact that something in your lockfile drifted in the first place. It works here specifically because it forces npm to re-resolve every copy of tailwindcss against the version now pinned in package.json, which a plain npm install won't always do once a lockfile has already committed to an older resolution.

An install that can't quietly drift out of sync

The deeper fix is making it hard for a config and a dependency to disagree in the first place. Two things:

Pin tailwindcss and @tailwindcss/postcss (or @tailwindcss/vite) to the exact same version, not a caret range, and bump them together with a single command rather than editing package.json by hand:

bash

npm install tailwindcss@latest @tailwindcss/postcss@latest

And prefer the Vite-native or CLI-native adaptor over the PostCSS one whenever your bundler offers it. Every extra adaptor in the chain, PostCSS itself, postcss-import, autoprefixer, is one more place a stale reference can hide after an upgrade. Fewer moving parts, fewer ways for the moving parts to fall out of step.

Wiring CI so this fails before it ships

This particular error is loud and blocks the build, so it'll never reach production silently. The danger isn't a broken deploy, it's the hour someone loses re-discovering this same fix six months from now. A few things make that hour shorter or unnecessary:

Commit the lockfile, and add a CI step that runs npm ci instead of npm install. ci refuses to proceed if package.json and the lockfile disagree, which catches a half-finished upgrade before it reaches a developer's laptop instead of after.

Add a one-line postinstall sanity check in monorepos with more than one Tailwind consumer:

bash

npm ls tailwindcss --all | grep -c "tailwindcss@" | awk '{ if ($1 > 1) { print "Multiple tailwindcss versions detected"; exit 1 } }'

It's blunt, but blunt is fine for a check that only exists to catch exactly this.

Renovate or Dependabot configured to bump tailwindcss and its adaptor package in the same pull request, never separately. Most dependency bots do this by default when both packages are listed, but it's worth confirming rather than assuming, especially in a monorepo with multiple package.json files.

Worth pinning

Tailwind v4 split its PostCSS plugin into @tailwindcss/postcss: a bare tailwindcss entry in your plugin list is a v3 leftover, not a typo. Run npm ls tailwindcss --all before you touch a single config file; it tells you in one line whether you're fixing a config mistake or a dependency mismatch. On Vite, skip PostCSS altogether and use @tailwindcss/vite, one less file to keep in sync. Pin the Tailwind package and its adaptor to matching versions and bump them together, and let npm ci in CI catch the half-upgraded state before a teammate does.

csstailwind-csspostcsssasscss-build-errorsvitenextjsfrontend

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