Writing a PostCSS Plugin
Links
Documentation:
Support:
- Ask questions
- PostCSS twitter with latest updates.
Step 1: Create an idea
There are many fields where writing new PostCSS plugin will help your work:
- Compatibility fixes: if you always forget to add hack
for browser compatibility, you can create PostCSS plugin to automatically
insert this hack for you.
postcss-flexbugs-fixes
andpostcss-100vh-fix
are good examples. - Automate routine operations: let’s computer do routine operations, free yourself for creative tasks. For instance, PostCSS with RTLCSS can automatically convert a design to right-to-left languages (like Arabic or Hebrew) or with postcss-dark-theme-class` can insert media queries for dark/light theme switcher.
- Preventing popular mistakes: “if an error happened twice, it will happen again.” PostCSS plugin can check your source code for popular mistakes and save your time for unnecessary debugging. The best way to do it is to write new Stylelint plugin (Stylelint uses PostCSS inside).
- Increasing code maintainability: CSS Modules or
postcss-autoreset
are great example how PostCSS can increase code maintainability by isolation. - Polyfills: we already have a lot polyfills for CSS drafts
in
postcss-preset-env
. If you find a new draft, you can add a new plugin and send it to this preset. - New CSS syntax: we recommend avoiding adding new syntax to CSS.
If you want to add a new feature, it is always better to write a CSS draft
proposal, send it to CSSWG and then implement polyfill.
postcss-easing-gradients
with this proposal is a good example. However, there are a lot of cases when you can’t send a proposal. For instance, browser’s parser performance limited CSSWG nested syntax a lot and you may want to have non-official Sass-like syntax from `postcss-nested.
Step 2: Create a project
There are two ways to write a plugin:
- Create a private plugin. Use this way only if the plugin is related to specific things of projects. For instance, you want to automate a specific task for your unique UI library.
- Publish a public plugin. It is always the recommended way. Remember that private front-end systems, even in Google, often became unmaintained. On the other hand, many popular plugins were created during the work on a closed source project.
For private plugin:
- Create a new file in
postcss/
folder with the name of your plugin. - Copy plugin template from our boilerplate.
For public plugins:
- Use the guide in PostCSS plugin boilerplate to create a plugin directory.
- Create a repository on GitHub or GitLab.
- 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:
- Find something in CSS (for instance,
will-change
property). - Change found elements (for instance, insert
transform: translateZ(0)
beforewill-change
as a polyfill for old browsers).
PostCSS parses CSS to the tree of nodes (we call it AST). This tree may content:
Root
: node of the top of the tree, which represent CSS file.AtRule
: statements begin with@
like@charset "UTF-8"
or@media (screen) {}
.Rule
: selector with declaration inside. For instanceinput, button {}
.Declaration
: key-value pair likecolor: black
;Comment
: stand-alone comment. Comments inside selectors, at-rule parameters and values are stored in node’sraws
property.
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:
- Selector parser
- Value parser
- Dimension parser
for
number
,length
andpercentage
. - Media query parser
- Font parser
- Sides parser
for
margin
,padding
andborder
properties.
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.