PostCSS received a major update with the release of version 8.0, codenamed “President Ose”. Plugin creators can now opt in for a new API that сan increase build speeds and reduce the size of dependencies for the end-users of their tools. This guide describes the steps you need to take as a plugin developer to make the most out of the improved framework.
PostCSS—a framework for processing CSS with JavaScript—is one of the most popular frontend tools for the modern web, with over 25 million downloads a week.
So much code depends on PostCSS since it’s trusted by big projects like webpack or Rails, and because it’s a universe of plugins that improve the way frontend developers write CSS.
Simple JavaScript rules can either automate routine tasks, like linting and adding vendor prefixes, or enable novel ways of creating stylesheets that are not directly supported by current web standards.

Irina Nazarova CEO at Evil Martians
If you develop or maintain plugins for PostCSS—this post is for you. It lists the main things you should do to make your code conform to the latest version of the framework.
Take a look at the full description of the latest release on GitHub to learn what else is new in PostCSS 8.0, including better source maps support and more resilient CSS parsing.
Why do I need to update my plugin?
The previous versions of PostCSS had some limitations with regards to plugins:
- Speed. Even if your plugin changes just a few style properties, it walks through the entire Abstract Syntax Tree of a CSS bundle. As different plugins are often used together, each of them needs to pass the whole tree on any property update—slowing down CSS builds for end-users.
- Size of node_modules. Plugins can list different versions of PostCSS under their
dependencies. This can cause the resultingnode_modulesfolder to bloat if npm’s deduplication fails to do its job. - Compatibility. Plugins designed for older versions of PostCSS can use deprecated ways of building nodes (e.g.,
postcss.decl()). Mixing AST nodes created by different PostCSS versions can cause painful bugs.
Step 1: Move postcss to peerDependencies
The first step is very simple. Just remove PostCSS version 7 from your dependencies and add PostCSS version 8 to devDependencies.
npm uninstall postcss
npm install postcss --save-devThen, move PostCSS 8 to peerDependencies by editing your package.json:
"dependencies": {
- "postcss": "^7.0.10"
},
"devDependencies": {
+ "postcss": "^8.0.0"
},
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
}This will keep the size of the end-user’s node_modules directory under control: now, all plugins will use the same version of postcss as a dependency.
If your dependencies object is now empty, feel free to remove it:
- "dependencies": {
- }
"devDependencies": {Also, don’t forget to change the installation instructions in your plugin’s documentation:
- npm install --save-dev postcss-focus
+ npm install --save-dev postcss postcss-focusStep 2: Use the updated API
- Replace
plugin = postcss.plugin(name, creator)with justplugin = creator. - Return an object with
postcssPluginproperty containing a plugin name and theOncemethod. - Move the plugin code to the
Oncemethod. - Add
plugin.postcss = trueto the end of the file.
Before:
- const plugin = postcss.plugin('postcss-example', (opts = {}) => {
- checkOpts(opts)
- return (root, result) => {
root.walkAtRules(atrule => { … })
- }
- })
module.exports = pluginAfter:
+ const plugin = (opts = {}) => {
+ checkOpts(opts)
+ return {
+ postcssPlugin: 'postcss-example',
+ Once (root, { result }) {
root.walkAtRules(atrule => { … })
+ }
+ }
+ }
+ plugin.postcss = true
module.exports = pluginDon’t forget to set plugin.postcss = true. This allows PostCSS to distinguish between require('plugin') and require('plugin')(opts) end-user calls.
Step 3: Fully embrace the new API
PostCSS 8 does a single CSS tree scan. Multiple plugins can leverage the same scan for better performance.
To use single scans, you need to remove root.walk* calls and move the code to the Declaration(), Rule(), AtRule() or Comment() methods in the plugin’s object:
const plugin = () => ({
postcssPlugin: 'postcss-example',
- Once (root) {
- root.walkAtRules(atRule => {
- // Slow
- })
- }
+ AtRule (atRule) {
+ // Faster
+ }
})
plugin.postcss = trueThe full list of plugin events can be found in the API docs.
The AtRule listener will visit the node again if someone changes the at-rule parameters or any children inside the at-rule.
It’s important to avoid plugin freeze: the plugin adds children to the at-rule, PostCSS calls the plugin again on this at-rule, the plugin adds another child.
const plugin = () => ({
postcssPlugin: 'postcss-example',
AtRule (atRule) {
if (atRule.every(child => child.selector !== '.test')) {
atRule.append({ selector: '.test' })
}
}
})
plugin.postcss = trueFor declarations and at-rules you can make your code even faster by subscribing to a specific declaration property or an at-rule name:
const plugin = () => ({
postcssPlugin: 'postcss-example',
- AtRule (atRule) {
- if (atRule.name === 'media') {
- // Faster
- }
- }
+ AtRule: {
+ media: atRule => {
+ // The fastest
+ }
+ }
}
plugin.postcss = trueNote that plugins will re-visit all changed or added nodes. You should check if your transformations were already applied, and if that is the case, ignore those nodes. Only the Once and OnceExit listeners will be called, exactly once.
const plugin = () => ({
Declaration(decl) {
console.log(decl.toString())
decl.value = "red"
}
})
plugin.postcss = true
await postcss([plugin]).process("a { color: black }", { from })
// => color: black
// => color: redIf you have a large plugin that is hard to rewrite, it’s OK to keep using walk method inside your Root listener.
There are two types of listeners: for “enter” and for “exit”. Once, Root, AtRule, or Rule will be called before processing children. OnceExit, RootExit, AtRuleExit, and RuleExit—after processing all the children of the node.
If you need a way to share data between listeners, you can use prepare():
const plugin = (opts = {}) => ({
postcssPlugin: 'postcss-example',
prepare(result) {
const variables = {}
return {
Declaration(node) {
if (node.variable) {
variables[node.prop] = node.value
}
},
OnceExit() {
console.log(variables)
}
}
}
})Step 4: Remove postcss imports
With the new PostCSS plugin API, you do not need to import postcss. You will get all classes and methods as a second argument to your Root function:
- const { list, Declaration } = require('postcss')
const plugin = () => ({
postcssPlugin: 'postcss-example',
- Once (root) {
+ Once (root, { list, Declaration }) {
…
}
})
plugin.postcss = trueThis is also something that you can keep as-is for a while if you don’t feel like doing it immediately.
Step 5: Reduce the npm package size
That is not a mandatory step, but we want to promote an amazing tool for keeping your package.json clean of development configs before publishing your package to npm: clean-publish. If the PostCSS ecosystem starts to use it, we will collectively make node_modules even smaller.
Add it to your project:
npm install --save-dev clean-publishNow use npx clean-publish instead of npm publish.
You can use the official Plugin Boilerplate and these plugins as an example for the adoption of a new API:
If you are looking for a commercial advice on a custom PostCSS integration, thinking about setting up better development practices for your technical team or need a revision of your product’s frontend infrastructure—feel free to contact Evil Martians.

