Writing a PostCSS Plugin

Documentation:

Support:

Step 1: Create an idea

There are many fields where writing new PostCSS plugin will help your work:

Step 2: Create a project

There are two ways to write a plugin:

For private plugin:

  1. Create a new file in postcss/ folder with the name of your plugin.
  2. Copy plugin template from our boilerplate.

For public plugins:

  1. Use the guide in PostCSS plugin boilerplate to create a plugin directory.
  2. Create a repository on GitHub or GitLab.
  3. Publish your code there.
module.exports = (opts = {}) => {
  // Plugin creator to check options or prepare shared state
  return {
    postcssPlugin: 'PLUGIN NAME'
    // Plugin listeners
  }
}
module.exports.postcss = true

Step 3: Find nodes

Most of the PostCSS plugins do two things:

  1. Find something in CSS (for instance, will-change property).
  2. Change found elements (for instance, insert transform: translateZ(0) before will-change as a polyfill for old browsers).

PostCSS parses CSS to the tree of nodes (we call it AST). This tree may content:

You can use AST Explorer to learn how PostCSS convert different CSS to AST.

You can find all nodes with specific types by adding method to plugin object:

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'PLUGIN NAME',
    Once (root) {
      // Calls once per file, since every file has single Root
    },
    Declaration (decl) {
      // All declaration nodes
    }
  }
}
module.exports.postcss = true

Here is the full list of plugin’s events.

If you need declaration or at-rule with specific names, you can use quick search:

    Declaration: {
      color: decl => {
        // All `color` declarations
      }
      '*': decl => {
        // All declarations
      }
    },
    AtRule: {
      media: atRule => {
        // All @media at-rules
      }
    }

For other cases, you can use regular expressions or specific parsers:

Other tools to analyze AST:

Don’t forget that regular expression and parsers are heavy tasks. You can use String#includes() quick test before check node with heavy tool:

if (decl.value.includes('gradient(')) {
  let value = valueParser(decl.value)
  …
}

There two types or listeners: enter and exit. Once, Root, AtRule, and Rule will be called before processing children. OnceExit, RootExit, AtRuleExit, and RuleExit after processing all children inside node.

You may want to re-use some data between listeners. You can do with runtime-defined listeners:

module.exports = (opts = {}) => {
  return {
    postcssPlugin: 'vars-collector',
    prepare (result) {
      const variables = {}
      return {
        Declaration (node) {
          if (node.variable) {
            variables[node.prop] = node.value
          }
        },
        OnceExit () {
          console.log(variables)
        }
      }
    }
  }
}

You can use prepare() to generate listeners dynamically. For instance, to use Browserslist to get declaration properties.

Step 4: Change nodes

When you find the right nodes, you will need to change them or to insert/delete other nodes around.

PostCSS node has a DOM-like API to transform AST. Check out our API docs. Nodes has methods to travel around (like Node#next or Node#parent), look to children (like Container#some), remove a node or add a new node inside.

Plugin’s methods will receive node creators in second argument:

    Declaration (node, { Rule }) {
      let newRule = new Rule({ selector: 'a', source: node.source })
      node.root().append(newRule)
      newRule.append(node)
    }

If you added new nodes, it is important to copy Node#source to generate correct source maps.

Plugins will re-visit all nodes, which you changed or added. If you will change any children, plugin will re-visit parent as well. Only Once and OnceExit will not be called again.

const plugin = () => {
  return {
    postcssPlugin: 'to-red',
    Rule (rule) {
      console.log(rule.toString())
    },
    Declaration (decl) {
      console.log(decl.toString())
      decl.value = 'red'
    }
  }
}
plugin.postcss = true

await postcss([plugin]).process('a { color: black }', { from })
// => a { color: black }
// => color: black
// => a { color: red }
// => color: red

Since visitors will re-visit node on any changes, just adding children will cause an infinite loop. To prevent it, you need to check that you already processed this node:

    Declaration: {
      'will-change': decl => {
        if (decl.parent.some(decl => decl.prop === 'transform')) {
          decl.cloneBefore({ prop: 'transform', value: 'translate3d(0, 0, 0)' })
        }
      }
    }

You can also use Symbol to mark processed nodes:

const processed = Symbol('processed')

const plugin = () => {
  return {
    postcssPlugin: 'example',
    Rule (rule) {
      if (!rule[processed]) {
        process(rule)
        rule[processed] = true
      }
    }
  }
}
plugin.postcss = true

Second argument also have result object to add warnings:

    Declaration: {
      bad: (decl, { result }) {
        decl.warn(result, 'Deprecated property bad')
      }
    }

If your plugin depends on another file, you can attach a message to result to signify to runners (webpack, Gulp etc.) that they should rebuild the CSS when this file changes:

    AtRule: {
      import: (atRule, { result }) {
        const importedFile = parseImport(atRule)
        result.messages.push({
          type: 'dependency',
          plugin: 'postcss-import',
          file: importedFile,
          parent: result.opts.from
        })
      }
    }

If the dependency is a directory you should use the dir-dependency message type instead:

result.messages.push({
  type: 'dir-dependency',
  plugin: 'postcss-import',
  dir: importedDir,
  parent: result.opts.from
})

If you find an syntax error (for instance, undefined custom property), you can throw a special error:

if (!variables[name]) {
  throw decl.error(`Unknown variable ${name}`, { word: name })
}

Step 5: Fight with frustration

I hate programming
I hate programming
I hate programming
It works!
I love programming

You will have bugs and a minimum of 10 minutes in debugging even a simple plugin. You may found that simple origin idea will not work in real-world and you need to change everything.

Don’t worry. Every bug is findable, and finding another solution may make your plugin even better.

Start from writing tests. Plugin boilerplate has a test template in index.test.js. Call npx jest to test your plugin.

Use Node.js debugger in your text editor or just console.log to debug the code.

PostCSS community can help you since we are all experiencing the same problems. Don’t afraid to ask in special channel.

Step 6: Make it public

When your plugin is ready, call npx clean-publish in your repository. clean-publish is a tool to remove development configs from the npm package. We added this tool to our plugin boilerplate.

Write a tweet about your new plugin (even if it is a small one) with @postcss mention. Or tell about your plugin in [our chat]. We will help you with marketing.

Add your new plugin to PostCSS plugin catalog.