What this usually means
PostCSS plugins are applied in a specific order, and the pipeline often fails silently. The most common root cause is that PostCSS is not reading the config file you think it is, or the config is malformed in a way that causes the plugin list to be empty. Another frequent culprit is that a build tool wrapper (like `postcss-loader` for webpack) has its own config override that doesn't include the plugin. When plugins do load but don't transform, it's usually because the plugin expects a certain syntax (e.g., PostCSS Preset Env requires stage settings) or the file is excluded by a `match` or `exclude` pattern. Caching in build tools can also make it appear that nothing changed.
The first ten minutes — establish facts before touching code.
- 1Run `npx postcss input.css -o output.css` — does the plugin work in isolation? If yes, the issue is in your build tool integration.
- 2Add `console.log(require('postcss-load-config').sync())` to your build script to see which config PostCSS actually loads.
- 3Check the loaded config's `plugins` array: ensure it's an array of plugin instances, not string names.
- 4Run with `DEBUG=postcss:*` environment variable — look for 'No plugins loaded' or plugin list.
- 5Clear build caches: `rm -rf node_modules/.cache` and restart. For webpack, also clear `node_modules/.cache/webpack`.
- 6Verify the plugin's `postcss` version compatibility: `npm ls postcss` and `npm ls <plugin-name>`.
The specific files, logs, configs, and dashboards that usually own this bug.
- search`postcss.config.js` (or .cjs, .mjs, .json, .yaml) — the file PostCSS loads automatically
- search`package.json` under `"postcss"` key — if present, PostCSS uses this instead of a config file
- searchBuild tool config: `webpack.config.js` for `postcss-loader` options, `vite.config.js` for `css.postcss`
- search`node_modules/.cache/` — stale caches from webpack, Vite, or PostCSS itself
- searchPlugin's own configuration — e.g., `preset-env` stage, `autoprefixer` browserslist
- search`browserslist` config in `package.json` or `.browserslistrc` — often affects autoprefixer and preset-env
- search`process.cwd()` — PostCSS resolves the config relative to the current working directory; verify it's correct
Practical causes, not theory. These are the things you will actually find.
- warningPostCSS loads a `postcss.config.js` that is empty or has an invalid `plugins` array (e.g., using strings instead of requiring the modules)
- warningBuild tool overrides the config — e.g., `postcss-loader` options `{ postcssOptions: { plugins: [] } }` that doesn't include the plugin
- warningPlugin is loaded but incorrectly configured — e.g., `postcss-preset-env` with `stage: 0` but using stage 2 features
- warningFile is excluded by a glob pattern in the plugin options (e.g., `postcss-import`'s `filter` option)
- warningCaching: the build tool serves stale output and never re-runs PostCSS on changed files
- warningMultiple PostCSS instances: the project has two versions of PostCSS (one in devDependencies, one in a dependency) causing incompatible plugins
- warningConfig file is in a subdirectory but PostCSS is run from the parent directory — uses a different cwd
Concrete fix directions. Pick the one that matches your root cause.
- buildExplicitly pass the config file path: `postcss({ config: './path/to/postcss.config.js' })` in build tool options
- buildIn webpack's `postcss-loader`, set `postcssOptions: { config: true }` to force loading from file
- buildUse `postcss-load-config` to programmatically load config and log it before passing to PostCSS
- buildAdd a dummy plugin like `postcss-reporter` to force visible output and confirm pipeline runs
- buildFor caching issues, set `cache: false` in `postcss-loader` options or add a version hash to the cache key
- buildRun `npx browserslist` to verify the browserslist query — if it returns empty, some plugins skip transforms
A fix you cannot prove is a guess. Close the loop.
- verifiedAdd a known CSS property like `color: rgba(255, 0, 0, 0.5)` and check if `postcss-preset-env` converts it to `rgba(255,0,0,.5)` or a fallback
- verifiedRun `npx postcss --verbose input.css -o output.css` and watch for plugin execution messages
- verifiedCheck the output file's source map — if plugins ran, the source map will show mappings from the input
- verifiedTemporarily add a plugin that throws an error (e.g., `postcss-simple-vars` with an undefined variable) — if it doesn't error, the plugin isn't running
- verifiedMonitor file change times: `stat -c '%Y' dist/bundle.css` before and after build — if unchanged, cache is serving stale content
Things that make this bug worse or harder to find.
- warningDon't assume `postcss.config.js` is loaded — always add a `console.log` at the top to confirm execution
- warningDon't mix `postcss.config.js` and inline config in build tool — one will override the other silently
- warningDon't forget that PostCSS resolves plugins relative to the config file's directory, not the current working directory
- warningDon't set `env` in PostCSS config unless you actually use environment-specific plugin lists
- warningDon't rely on `module.exports = { plugins: [...] }` if you have async plugins — use an array of plugin instances instead
The Autoprefixer That Never Ran
Timeline
- 09:15Deploy to staging. CSS bundle shows `display: flex` unprefixed (missing `-webkit-flex`).
- 09:17Check `postcss.config.js` — autoprefixer is listed in plugins array.
- 09:20Run `npx postcss src/index.css -o out.css` — prefixes appear correctly. So PostCSS works in CLI.
- 09:25Look at webpack config. Find `postcss-loader` options: `{ postcssOptions: { plugins: ['autoprefixer'] } }` — string, not function. That's deprecated but should work.
- 09:30Add `console.log` inside `postcss.config.js` — it never logs. The config file is not being loaded.
- 09:32Check `postcss-loader` docs. Realize that when `postcssOptions` is provided, it overrides the config file entirely.
- 09:35Remove the inline `postcssOptions.plugins` from webpack config. Set `postcssOptions: { config: './postcss.config.js' }`.
- 09:37Rebuild. Prefixes appear. Verified with `grep -o flex out.css | wc -l` — now 27 matches vs 9 before.
I was pushing a feature branch to staging and the CSS bundle looked off. The `display: flex` declarations weren't getting `-webkit-flex` prefixes. I checked the production build—it worked fine there. That was my first clue: the build tool integration was different.
I ran PostCSS from the CLI and it produced perfect output. So the plugin itself was fine. I added a `console.log` to the top of `postcss.config.js` and saw nothing in the webpack output. That's when I knew webpack was ignoring the config file entirely.
The root cause was that `postcss-loader`'s `postcssOptions` were overriding the config file. I had a legacy inline plugin list as strings, which worked with older PostCSS but was now being ignored. Removing the inline list and pointing `postcssOptions.config` to the file fixed it. The lesson: never mix inline and file-based PostCSS configuration.
Root cause
webpack's `postcss-loader` had both `postcssOptions.plugins` (inline) and a `postcss.config.js` file. The inline options took precedence, but the plugin list was provided as strings (deprecated) and PostCSS 8+ silently ignored them.
The fix
Removed the inline `postcssOptions.plugins` from the webpack config and added `postcssOptions: { config: './postcss.config.js' }`. This forced the loader to use the file-based config, which had proper plugin instances.
The lesson
Always verify which PostCSS config the build tool is actually loading. A quick `console.log` in the config file is the cheapest diagnostic. Never trust that inline options and config files merge—they don't.
PostCSS uses `postcss-load-config` (v4+) to find a config file. It looks for `postcss.config.js`, `postcss.config.cjs`, `postcss.config.mjs`, `postcss.config.json`, `postcss.config.yaml`, or a `"postcss"` key in `package.json`—in that order. The search starts from the current working directory (`process.cwd()`) and walks up the directory tree.
A common pitfall: build tools like webpack or Vite may change the cwd or provide their own `postcssOptions` that bypass this resolution entirely. If you pass `postcssOptions` to `postcss-loader`, the loader will not read any config file. Always check the build tool's documentation to know whether it uses the file or not.
PostCSS plugins are applied in array order. However, some plugins (like `postcss-import`) must run before others (like `postcss-preset-env`). If `postcss-import` runs after `preset-env`, the imports won't be transformed. This can cause the appearance that a plugin isn't working when it actually is, just on the wrong input.
Silent failures also happen when a plugin's configuration excludes certain files. For example, `postcss-import` has a `filter` option that can skip files matching a pattern. Similarly, `postcss-preset-env` only transforms features that match the declared browserslist—if browserslist returns an empty list, it transforms nothing.
Webpack's `postcss-loader` has a built-in cache that can become stale. If you change a PostCSS config or plugin, webpack may still serve the cached output. The same applies to Vite's CSS pipeline. Clearing `node_modules/.cache` is the first step, but you can also force-disable caching during development with `{ cache: false }` in the loader options.
Another caching layer is the PostCSS plugin itself. Some plugins (like `cssnano`) have internal caches based on file content hashes. If a file hasn't changed on disk (e.g., because your editor auto-save didn't trigger a proper write), the plugin may skip it. Use `touch` on the source file to force a re-build.
A particularly nasty cause is having two versions of PostCSS in your dependency tree. Plugins are often pinned to a specific PostCSS version range. If you have both PostCSS 7 and 8 installed (e.g., a legacy plugin that hasn't updated), the wrong one may be loaded. Run `npm ls postcss` to see if there are multiple versions.
The same applies to plugins themselves. If a plugin is installed but its peer dependency on PostCSS isn't satisfied, it may silently fail to load. Always check the plugin's `package.json` for `peerDependencies` and verify they match your PostCSS version.
The fastest way to isolate a PostCSS issue is to bypass the build tool entirely. Run: `npx postcss --verbose --config /path/to/postcss.config.js input.css -o output.css`. The `--verbose` flag prints which plugins are loaded and in what order.
If the CLI works but the build tool doesn't, the problem is in the integration. Compare the output of `npx postcss --env` (which shows parsed config) with what your build tool is using. You can also set `DEBUG=postcss:*` to get detailed logs from `postcss-load-config` and the plugin pipeline.
Frequently asked questions
Why does my PostCSS plugin work in the CLI but not in webpack?
Most likely webpack's `postcss-loader` is not reading the same config file. Check your `webpack.config.js` for `postcssOptions` — if present, it overrides the config file. Set `postcssOptions: { config: true }` to force it to load the file-based config.
What does 'No plugins loaded' mean in PostCSS debug output?
It means the `plugins` array in your config is empty or the config file wasn't loaded. Look at your `postcss.config.js` — ensure `module.exports` exports an object with a `plugins` key that is an array of plugin instances (not strings). Also verify the file is in the expected directory relative to the build tool's cwd.
My PostCSS preset-env isn't transforming anything even though I have `stage: 0`—why?
Check your browserslist config. If it returns an empty list or only very modern browsers, preset-env may decide no transforms are needed. Run `npx browserslist` to see what browsers you've targeted. Also ensure you're using the right stage for your features—stage 2 features won't transform with `stage: 3`.
Can I merge inline PostCSS options with a config file?
No. If you provide `postcssOptions` in your build tool (like webpack or Vite), it completely replaces the file-based config. You must either use one or the other. To use a config file, set `postcssOptions: { config: './postcss.config.js' }` without specifying plugins.
I cleared the cache but my PostCSS plugin still doesn't work—what else could it be?
Check if the plugin is actually loaded by adding a plugin that always outputs something (like `postcss-reporter`). Also verify that the plugin's peer dependencies are satisfied. Run `npm ls postcss` to check for multiple versions. Finally, ensure your source file isn't excluded by a plugin's `match` or `exclude` options.