The PostCSS node tree
PostCSS turns a stylesheet into a tree of typed nodes. A Root contains
Rule and AtRule children; rules and at-rules contain Declaration and
Comment nodes. Plugins walk this tree, read properties like selector,
prop and value, and mutate or replace nodes — then PostCSS stringifies the
tree back to CSS. This reference lists each built-in node type and the API you
use to transform it.
How it works
Every node carries a type string and links to its parent. Containers
(Root, Rule, AtRule) hold a nodes array of children and expose walk*
helpers to traverse them:
module.exports = () => ({
postcssPlugin: "demo",
Once(root, { Declaration }) {
root.walkDecls("color", (decl) => {
if (decl.value === "red") decl.value = "#f00";
});
root.walkAtRules("media", (atRule) => {
// atRule.name === "media", atRule.params === "(min-width: 768px)"
});
},
});
module.exports.postcss = true;
Walk callbacks run depth-first. Returning nothing continues; calling
decl.remove() or rule.replaceWith(...) mutates the tree in place.
Node type quick reference
| Type | Identifies | Key properties |
|---|---|---|
root | The whole document | nodes array of children |
rule | A selector block | selector string, nodes |
atrule | @ keyword | name (e.g. media), params, nodes (undefined for statement at-rules) |
decl | A property: value pair | prop, value, important (boolean) |
comment | /* CSS comment */ | text (contents without delimiters) |
Visitor API vs Once + walkDecls
PostCSS 8 introduced a visitor API where your plugin exports a plain object with method names matching node types:
// Visitor API — PostCSS batches a single pass over all plugins
module.exports = {
postcssPlugin: "my-plugin",
Declaration(decl) {
if (decl.prop === "color" && decl.value === "red") {
decl.value = "#f00";
}
},
};
module.exports.postcss = true;
The visitor API is preferred because PostCSS can combine multiple plugins into a single AST
traversal, while Once + walkDecls forces a separate pass per plugin. For complex plugins that
need to see the whole tree before acting, Once is still appropriate.
Common mutations
// Remove a declaration
decl.remove();
// Replace a rule with a comment
rule.replaceWith(postcss.comment({ text: "removed" }));
// Append a new declaration to a rule
rule.append({ prop: "display", value: "flex" });
// Clone before inserting elsewhere
const copy = decl.clone({ value: "new-value" });
rule.append(copy);
Tips and notes
- Prefer the visitor API (
Declaration(decl, helpers)) overOnce+walk*for plugins that compose well — PostCSS batches a single AST pass. - Use
node.clone()before reusing a node elsewhere; nodes can only have one parent. decl.importantis a boolean, not part ofvalue; preserve it when rewriting.atRule.nodesisundefinedfor statement at-rules like@import, and an array for block at-rules like@media.- Check
node.sourcefor the original position (file, line, column) when generating sourcemaps or producing useful error messages from your plugin.