postcss review
PostCSS 8.5.26 parses CSS into a source-aware node tree, runs JavaScript visitors over it, and serializes the result. The core package does not add prefixes, lower future syntax, nest selectors, lint, or minify; separate plugins perform each of those jobs. Version 8.5.26 repairs a list.split regression and tracks symlinks while protecting source-map paths. Our full import produced 56.5 KB minified and 18.2 KB gzipped. That cost is normal in build tooling but harder to justify inside an end-user browser feature.
PostCSS 8.5.26 installed in 0.4 seconds as 7 packages and bundled to 18.2 KB gzipped in our sandbox, with 0 audit findings. Add it for a named plugin chain or AST task; if the framework already owns CSS or a compiled transformer covers the whole job, a second PostCSS setup is unnecessary.
We installed it
| Install | ✓ · 0.4s | 7 packages on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 18.2 KB | gzipped (56.5 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does postcss install cleanly?
Yes. In a fresh container with an empty cache, npm install postcss finished in 0.4s, leaving 7 packages and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does postcss add to a browser bundle?
18.2 KB gzipped (56.5 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does postcss work with both ESM and CommonJS?
Yes. Both import 'postcss' and require('postcss') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does postcss include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
postcss or lightningcss: which should you use?
lightningcss: Choose it for compiled prefixing, syntax transforms, and minification without a JavaScript visitor chain. PostCSS 8.5.26 installed in 0.4 seconds as 7 packages and bundled to 18.2 KB gzipped in our sandbox, with 0 audit findings.
When should you not use postcss?
The framework already emits the desired CSS and offers no custom transform requirement; a direct PostCSS dependency would add unused configuration.
Discussed on
- hnRails 6 with Webpacker 6, Tailwind 2 with JIT, Postcss 8 and some default setup169 points
- hnA PostCSS plugin for people who prefer to write English properly84 points
- hnHow PostCSS became 1.5x faster by changing 2 lines of code55 points
- hnCSSNano: A modular minifier based on the PostCSS ecosystem31 points
- hnShow HN: Rucksack – CSS superpowers, built on PostCSS27 points
Use it if
- You are writing a CSS codemod, linter, or migration that needs nodes with file, line, and formatting information.
- A build requires an ordered set of specific PostCSS plugins such as Autoprefixer or postcss-preset-env.
- The current framework already loads a PostCSS config and one additional transform fits that established pipeline.
- A tool must parse SCSS-like, Less-like, HTML-embedded, or CSS-in-JS syntax through a compatible parser without evaluating the source language.
- The framework already emits the desired CSS and offers no custom transform requirement; a direct PostCSS dependency would add unused configuration.
- Prefixing, syntax lowering, and minification should come from one compiled tool. Lightning CSS has a narrower pipeline with fewer plugin versions.
- You need Sass modules, functions, or mixins. postcss-scss can parse SCSS syntax but does not execute Sass.
- You expect PostCSS core to change CSS by itself. An empty plugin array parses and prints without adding application behavior.
- The parser would ship to a page for a minor edit feature. Our complete import cost 56.5 KB minified and 18.2 KB gzipped before any plugin code.
Setup reality
We installed PostCSS 8.5.26 in a clean Node 22 Bookworm sandbox. npm completed in 0.4 seconds and left 7 packages using 1 MB. PostCSS itself was 352 KB unpacked with 3 direct dependencies and no peers. npm audit reported 0 known vulnerabilities. The package is CommonJS behind an exports map, bundles TypeScript declarations, and worked through both require() and ESM import. Its engine range is Node ^10, ^12, or >=14.
Core discovers no plugins and reads no config by itself. Pass plugins to postcss([...]) or use a bundler adapter that invokes postcss-load-config. Config extensions and export syntax must match that adapter and the project's module mode. Plugin sequence affects output, particularly when syntax lowering runs before prefix generation or minification. No credentials are involved unless an individual plugin introduces them.
Pass from for real source so warnings, error frames, URL resolution, and maps name the correct file; add to for a known destination. The object returned by process() is lazy. Await it even when today's plugins are synchronous, because a later async plugin makes direct lazy.css access throw. A successful result can still contain warnings, so builds need an explicit result.warnings() policy.
PostCSS 8 schedules changed nodes for another visit. A visitor that inserts a matching node or repeatedly mutates the same declaration can loop until memory is exhausted; make transforms idempotent or mark handled nodes. Custom syntax packages may parse foreign notation without compiling its semantics. Our esbuild namespace import measured 56.5 KB minified and 18.2 KB gzipped, and many plugins also assume filesystem or Node resolution, so browser execution needs individual compatibility checks.
Patterns
Process CSS with one plugin run-autoprefixer
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
const result = await postcss([autoprefixer]).process(css, {
from: 'src/main.css',
to: 'dist/main.css',
});from supplies file identity to warnings and maps. Add to when output has a real destination.
Write a declaration visitor create-declaration-plugin
const pxToRem = () => ({
postcssPlugin: 'px-to-rem',
Declaration(decl) {
if (decl.value.includes('px')) {
decl.value = decl.value.replace(/(\d+)px/g, (_, n) => `${n / 16}rem`);
}
},
});
pxToRem.postcss = true;A PostCSS 8 plugin creator returns an object named with postcssPlugin. Make the replacement idempotent because changed nodes can be visited again.
Rewrite matching nodes walk-selected-nodes
const root = postcss.parse(css);
root.walkDecls('color', decl => { decl.value = 'var(--brand)'; });
root.walkRules(/^\.button/, rule => { rule.selector += ':not([disabled])'; });Filtered walkers reduce calls. A mutation that still matches must settle on the next visit.
Read parsed fields inspect-node-data
const root = postcss.parse('a { color: black }', { from: 'input.css' });
const rule = root.first;
console.log(rule.type, rule.selector);
console.log(rule.first.prop, rule.first.value);Semantic values use named fields, while spacing and punctuation details live under raws.
Build a media rule construct-at-rule
const rule = postcss.rule({ selector: '.card' });
rule.append(postcss.decl({ prop: 'padding', value: '1rem' }));
const media = postcss.atRule({ name: 'media', params: '(min-width: 600px)' });
media.append(rule);
root.append(media);New nodes have no neighboring formatting context. Clone an existing node when a codemod should minimize whitespace changes.
Configure a shared pipeline declare-plugin-order
// postcss.config.js
export default {
plugins: {
'postcss-preset-env': { stage: 2 },
autoprefixer: {},
},
};A build adapter loads this through postcss-load-config. Confirm its ESM, CommonJS, and filename rules.
Write an external source map emit-source-map
const result = await postcss(plugins).process(css, {
from: 'src/main.css', to: 'dist/main.css', map: { inline: false },
});
await writeFile('dist/main.css', result.css);
await writeFile('dist/main.css.map', result.map.toString());When inline is false, application code must write the map beside the CSS and preserve its sourceMappingURL relationship.
Handle asynchronous plugins await-lazy-result
const lazy = postcss([plugin]).process(css, { from: 'input.css' });
const result = await lazy;
console.log(result.css);Direct lazy.css access throws when any plugin is asynchronous. Awaiting works for both synchronous and async pipelines.
Print a syntax error frame format-css-error
try {
postcss.parse(broken, { from: 'theme.css' });
} catch (error) {
if (error instanceof postcss.CssSyntaxError) console.error(error.showSourceCode(true));
else throw error;
}CssSyntaxError also exposes file, line, and column for machine-readable build output.
Mark inserted fallbacks prevent-revisit-loop
const fallback = () => ({
postcssPlugin: 'inset-fallback',
Declaration(decl) {
if (decl.prop !== 'inset' || decl.raws.handled) return;
decl.cloneBefore({ prop: 'top', value: decl.value, raws: { handled: true } });
decl.raws.handled = true;
},
});PostCSS revisits changed nodes, so a visitor that keeps inserting matches can run indefinitely.
Promote warnings to build failures fail-on-warnings
const result = await postcss(plugins).process(css, { from: input });
for (const warning of result.warnings()) console.error(warning.toString());
if (result.warnings().length > 0) process.exitCode = 1;A transform can finish successfully and still carry plugin warnings. The build wrapper decides whether they are fatal.
Insert a compatible color clone-fallback-declaration
root.walkDecls('color', decl => {
if (decl.value === 'oklch(60% 0.2 30)') decl.cloneBefore({ value: '#c44' });
});cloneBefore retains source and formatting metadata. Matching only the original value keeps the new fallback out of the visitor.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lightningcss | npm | Choose it for compiled prefixing, syntax transforms, and minification without a JavaScript visitor chain. |
| sass | npm | Choose it when Sass evaluation, modules, mixins, and functions define the source language. |
| less | npm | Choose it for an existing Less codebase that depends on its variables, imports, and mixins. |
More web frontend guides
react · react-dom · tailwindcss · htmlparser2 · tailwind-merge · class-variance-authority · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

