CSSPostCSS

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 browserslist config 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 @import files at build time instead of leaving real HTTP @import requests in the shipped CSS.

  • stylelint (technically its own tool, but PostCSS-based) — lints CSS for consistency and bugs. See Stylelint.

Configuring PostCSS

HTML
npm install -D postcss autoprefixer postcss-preset-env cssnano

SCSS
// 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 matterspostcss-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.

Note
If you're not sure whether your framework already runs autoprefixer, check the compiled CSS in the browser's dev tools for-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.

SCSS
// 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 = true
  • PostCSS calls visitor methods like Rule, Declaration, and AtRule on 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.

Tip
You rarely need to write a PostCSS plugin yourself — the value of knowing this shape is mostly for *reading* what an existing plugin does when its behavior surprises you in the compiled output.