PostCSS
PostCSS is a tool for transforming CSS with JavaScript plugins. That's the whole idea: it parses your CSS into an AST (Abstract Syntax Tree — a structured object representation of the rules, selectors, and declarations), lets a chain of plugins walk and mutate that tree, then prints CSS back out. What the tool actually does to your CSS is entirely defined by which plugins you install — PostCSS itself has almost no opinions.
PostCSS is not a preprocessor
This is the most common misconception. Sass and Less are preprocessors — they define their own superset language (@if, @each, @variable) that must be compiled to CSS before a browser ever sees it. PostCSS defines no language of its own; it's a transformer that operates on CSS (or CSS-like syntax) via plugins. You could build a Sass-like preprocessor as a stack of PostCSS plugins, and some projects do — but PostCSS's most common job is much more mundane: adding vendor prefixes, converting modern syntax to something older browsers understand, and minifying.
Preprocessor (Sass) | PostCSS | |
|---|---|---|
Defines its own syntax | Yes (SCSS) | No — plugins operate on CSS/near-CSS |
Single fixed feature set | Yes | No — entirely plugin-dependent |
Typical job | Nesting, variables, mixins, loops | Autoprefixing, polyfilling new syntax, linting, minifying |
Compile step required | Always | Only if you use plugins that need one |
The plugin ecosystem
autoprefixer — reads a
browserslistconfig and automatically adds vendor prefixes (-webkit-,-moz-) for properties that still need them in your target browsers. See Autoprefixer & Vendor Prefixes.postcss-preset-env — lets you write tomorrow's CSS syntax (nesting,
:has(), custom media queries) today, and transpiles/polyfills it down to what current browsers support, similar in spirit to Babel for JavaScript.cssnano — minifies the final CSS output: strips whitespace/comments, merges duplicate rules, shortens colors and values.
postcss-import — inlines
@importfiles at build time instead of leaving real HTTP@importrequests in the shipped CSS.stylelint (technically its own tool, but PostCSS-based) — lints CSS for consistency and bugs. See Stylelint.
Configuring PostCSS
npm install -D postcss autoprefixer postcss-preset-env cssnano
// postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')({ stage: 2 }),
require('autoprefixer'),
process.env.NODE_ENV === 'production' && require('cssnano'),
].filter(Boolean),
}Plugin order matters — postcss-preset-env should generally run before autoprefixer so autoprefixer sees the final, transpiled property names, and cssnano should run last since it assumes it's looking at browser-ready CSS.
PostCSS is already inside your build tool
Most developers use PostCSS every day without ever touching a config file. Next.js, Create React App, and Vite all run PostCSS internally with autoprefixing baked in; Tailwind CSS (see Tailwind CSS Introduction) is itself distributed as a PostCSS plugin. In this codebase's own next.config.mjs and build pipeline, PostCSS runs automatically for every stylesheet — there is usually no reason to hand-roll a config unless you need a plugin the framework doesn't already include.
-webkit-/-moz- prefixed properties on things like appearance or backdrop-filter — their presence is a strong signal PostCSS + autoprefixer is active.Writing a trivial plugin (conceptually)
A PostCSS plugin is just a function that receives the parsed AST and walks it. This sketch (not meant to run as-is) shows the shape: find every declaration and log a warning if it uses a hardcoded color instead of a custom property.
// A conceptual sketch of a PostCSS plugin's shape
module.exports = (opts = {}) => ({
postcssPlugin: 'no-hardcoded-colors',
Declaration(decl) {
const isColorProp = ['color', 'background-color', 'border-color'].includes(
decl.prop
)
const looksHardcoded = /^#|rgb\(|hsl\(/.test(decl.value)
if (isColorProp && looksHardcoded) {
decl.warn(decl.source.input.css, 'Use a custom property instead of a hardcoded color')
}
},
})
module.exports.postcss = truePostCSS calls visitor methods like
Rule,Declaration, andAtRuleon your plugin object for every matching node it walks — you never write your own tree-walking loop.A plugin can mutate nodes in place (
decl.value = '...'), insert new nodes (rule.append(...)), or just inspect and warn, as above.Because every plugin operates on the same shared AST in sequence, plugins compose — one plugin's output is the next plugin's input.
Related pages: Autoprefixer & Vendor Prefixes, Stylelint, CSS in Build Tools, and Tailwind CSS Introduction.