
The pipeline in one picture
Every PostCSS run follows the same path:
- Tokenize the CSS text into small pieces (words, brackets, strings, comments).
- Parse those tokens into an Abstract Syntax Tree (AST).
- Run plugins, which read and modify the AST.
- Stringify the AST back into CSS text, optionally with a source map.
PostCSS itself only owns steps 1, 2 and 4. Step 3 is entirely up to the plugins you install. That is why the core package is small: about 216 KB unpacked with just three dependencies.
Step 1: the tokenizer
The tokenizer reads the raw string character by character and groups it into tokens such as word, string, brackets, comment, { and }. It also tracks line and column numbers for every token, which is how error messages and source maps can point to exact positions later.
The tokenizer is also where edge cases live. The 2023 security fix (CVE-2023-44270) was a change to how carriage-return characters inside comments are tokenized.
Step 2: the parser and the AST
The parser assembles tokens into nodes. Each node has a type:
| Node | Represents | Example |
|---|---|---|
| Root | The whole file | everything in style.css |
| AtRule | An at-rule | @media (min-width: 600px) |
| Rule | A selector with a block | .card { … } |
| Declaration | A property and value | color: red |
| Comment | A comment | /* note */ |
| Document | A container for several roots | used by custom syntaxes such as HTML or Vue files |
You can see the tree yourself:
const postcss = require('postcss')
const root = postcss.parse('.card { color: red }')
console.log(root.first.selector) // .card
console.log(root.first.first.prop) // color
console.log(root.first.first.value) // redNodes also keep raws, the original whitespace and formatting. That lets PostCSS print untouched parts of the file exactly as you wrote them, which is important for linters and formatters.
Step 3: plugins
A plugin is an object with a postcssPlugin name and one or more listeners. The modern API (PostCSS 8) uses a visitor pattern: you subscribe to node types and PostCSS calls you as it walks the tree.
module.exports = (opts = {}) => {
return {
postcssPlugin: 'no-red',
Declaration (decl, { result }) {
if (decl.value === 'red') {
decl.warn(result, 'Use a color token instead of red')
}
}
}
}
module.exports.postcss = trueUseful listeners include:
OnceandOnceExit: run one time on the Root, before and after other listeners.Rule,AtRule,Declaration,Comment: run for each node of that type.- Keyed forms such as
AtRule: { media: … }orDeclaration: { color: … }to target specific names.
Because all plugins share one walk of the tree, PostCSS can revisit nodes that another plugin changed. That is what makes it possible to chain many plugins without each re-parsing the file.
Plugin order
Plugins run in the order you list them in your config. A nesting plugin must run before Autoprefixer so that nested rules are flattened first; a minifier such as cssnano should be last.
Step 4: stringify and source maps
When plugins finish, the stringifier walks the tree and writes CSS. Untouched nodes reuse their saved raws; new nodes get sensible default formatting. If you pass the map option, PostCSS builds a source map that links output positions to the original file, and it can merge an existing map from an earlier step such as Sass.
const postcss = require('postcss')
const autoprefixer = require('autoprefixer')
postcss([autoprefixer])
.process(css, { from: 'src/app.css', to: 'dist/app.css', map: { inline: false } })
.then(result => {
console.log(result.css) // final CSS
console.log(result.map.toString()) // source map
result.warnings().forEach(w => console.warn(w.toString()))
})Lazy results
process() returns a LazyResult. Plugins do not run until you read result.css, call .then() or similar. If you call process() with no plugins at all, PostCSS can skip work and simply return the input; internally that path is called NoWorkResult.
How build tools run PostCSS
You rarely call the API by hand. Runners do it for you:
- Vite detects a PostCSS config and applies it to imported CSS.
- webpack uses postcss-loader in the CSS rule chain.
- postcss-cli runs from npm scripts:
postcss src -d dist. - gulp-postcss and other integrations wrap the same API.
All of them read the same PostCSS config file.
Custom syntaxes
The parser and stringifier are swappable. With postcss-scss or postcss-less as the syntax option, PostCSS can parse SCSS or Less files without compiling them, which is how Stylelint lints those languages.
Summary
- PostCSS = tokenizer + parser + plugin runner + stringifier.
- The AST uses Root, AtRule, Rule, Declaration, Comment and Document nodes.
- Plugins subscribe to node types and edit the tree; order matters.
- Source maps and preserved raws keep output traceable and tidy.
Get PostCSS 8.5.32
Free, MIT licensed, downloaded from the official GitHub repository.


