Skip to content
April 30, 2017

PostCSS

Talking about PostCSS and making a plugin while at it

Probably my first post. So young, so naive.
Heads up, this article has been rewritten and posted on Medium 👌👌

Preface

These past few weeks I’ve been messing around with PostCSS. Everything about the said ecosystem is astounding and rightly so. Before we go any further, think of PostCSS as a preprocessor like your favourite one. Technically it’s not, but let’s take one step at a time.

First of all, what is it that makes the PostCSS’ approach much more attractive? Saving you some time, the answer is it’s modular approach, allowing the user to tweak the dependencies as he/she sees fit. PostCSS standalone, does nothing. It’s up to you to include everything that benefits your development process. It’s essentially a tool, waiting to feed your awesome css to a number of javascript plugins.

SASS and LESS are monolithic. You get everything, batteries included, and with that thousands of lines of code you might not need. While it’s great to have a black box that just works, as a developer I prefer an ecosystem where I can use -among others- my own, tailor-made plugins. And this folks, is the biggest sell of PostCSS. You can build ground up your CSS development process, exactly like you want.

Another distinct difference is that every pre-processor has it’s own idiomatic syntax, limited to it’s own use. Essentially you write code inside css. Why not just write CSS separately and have JavaScript do it’s ES6 magic afterwards? JavaScript isn’t going anywhere, can LESS say the same?

How it Works

Alright, let's take the example of Autoprefixer, the most famous PostCSS plugin. Minding your own business, you decide to align side by side these cat facts paragraphs. Something seems off and you notice that they don't have the same height. It's expected, but it would be nice to arrange them in a way that shadows or borders wouldn't seem silly.

One dirty but effective approach is the following:

CSS
style.css
.column-wrapper {
  overflow: hidden;
}
 
.column-wrapper .column {
  margin-bottom: -9999px;
  padding-bottom: 9999px;
  /* and whatever grid css rules you want */
}

I shiver just by looking at it. But thanks to Flexbox we have a great tool in our hands to solve such problems. As expected though, in order for a Flexbox solution to work in all modern browsers, we have to use some prefixes,

CSS
style.css
.element {
  box-shadow: 0 0 2px 1px #797979;
  display: flex;
}

Now, what we should do is include the whole -moz and -webkit spam. While it might be alright for a small app, in more ambitious ones we should search for alternatives.

In SASS you would create a mixin and have it add include the prefixes. A simple include doesn't seem much, but it's still suboptimal. What if in the following months, a prefix or two is no longer needed? Consider the box shadow case. Firefox & Chrome don't need prefixes for shadows, but SASS would add them anyway.

The ideal case is to ommit any prefix. Autoprefixer will parse the css code and add itself only the necessary ones. The sole thing you have to do is to point out which browser versions you support. Consulting the caniuse database, Autoprefixer will take care of the rest.

How is that possible? Earlier I said that PostCSS does nothing without plugins. I lied. It does one thing, which is to tranform your css code to Abstract Syntax Tree (AST). A JSON like data presentation, that every plugin will parse and modify. When everything is said and done, the output is stringified and ready for production.

Play around this example to get a better grasp of the whole AST transformation.

Notable Plugins

There are 200+ plugins out there, so it's hard to list the 'best' ones. What I can do though, is to post my postcss configuration for this very site.

const rucksack = require('rucksack-css');
const lost = require('lost');
const cssnext = require('postcss-cssnext');
const atImport = require('postcss-import');
const autocorrect = require('postcss-autocorrect');
const magician = require('postcss-font-magician');
 
exports.modifyWebpackConfig = function (config) {
  config.merge({
    postcss: [
      atImport(),
      magician({
        variants: {
          Raleway: {
            200: [],
            400: [],
            500: [],
          },
        },
      }),
      autocorrect(),
      rucksack(),
      cssnext({
        browsers: ['>1%', 'last 2 versions'],
      }),
      lost(),
    ],
  });
 
  return config;
};

CSSnext allows me to write CSS specifications not quite supported yet by any browser. Variables, Nesting, etc, will be available sooner or later. So why not use them today?

Font Magician is my favourite. When it came to decide which font to use, i wrote the following code:

CSS
style.css
body {
  /*font-family: 'Montserrat';*/
  /*font-family: 'Josefin Sans';*/
  /*font-family: 'Inconsolata';*/
  /*font-family: 'Fira Mono';*/
  /*font-family: 'Roboto';*/
  font-family: 'Raleway';
}

Hot reloading allowed me to change the font-family without any effort and check 6 fonts under 30 seconds. It's amazing. The site is extremely lightweight, so I can get away with Google fonts. If you'd rather use self-hosted fonts, Font Magician can help you out in this case too.

Rucksack is a collection of very handy plugins. It's responsive font-size plugin is pure awesomeness. I used to do the following in order to get responsive typography:

CSS
style.css
body {
  font-size: calc(0.5em + 1vw);
}

But with rucksack, I can just throw the following lines of code and be done with it

CSS
style.css
body {
    /* cssnext & font magician lines */
    background-color: var(--secondary-color);
    color: var(--primary-color);
    font-family: "Raleway"
    /* rucksack */
    font-size: responsive 14px 18px;
}

Of course you can set some breakpoints for the upper and lower limits. I like it because you can have global responsive typography but also target other elements in a nicer manner. I don't really like calc too much, and em units can be tricky from time to time. If you want to have some specific rules about a call-to-action section for example, just do this and avoid pixels and breakpoints.

CSS
style.css
& .call-to-action {
  font-size: responsive 18px 22px;
}

Lost is a nice to have plugin when you don't want to include some css grid framework. I don't use any in my side projects, so it saves some lines of code.

Finally postcss-autocorrect is a plugin I made.

There are moments when I make a typo and i wonder why nothing changed. In this plugin, I correct these typos, the flow continues and there is a warning in the console for the user. That's all.

Making a plugin

Boilerplates are great, so clone this repo to quickstart the project.

Every bit of logic will be placed in index.js. Nothing fancy, we get the options, if any, and parse the AST. Business as usual.

JavaScript
index.js
var postcss = require('postcss');
 
module.exports = postcss.plugin('PLUGIN_NAME', function (opts) {
  opts = opts || {};
 
  // Work with options here
 
  return function (root, result) {
    // Transform CSS AST here
  };
});

For our showcase, let's do something simple. Say we have the following code:

CSS
style.css
.link {
  text-decoration: none;
  color: red;
  @disable #efefef;
}

We will transform the @disable #efefef to:

CSS
style.css
.link {
  text-decoration: none;
  color: red;
  & .disabled {
    color: #efefef;
  }
}

Note that the & syntax is the way CSSnext works with nesting. Read more about the specification here.

Alright, lets get our hands dirty. Follow each step here. Developing in this platform is much more easy, since you can check AST any time. Babel is also included, so we can modify our code a bit like this:

JavaScript
index.js
import * as postcss from 'postcss';
 
export default postcss.plugin('postcss-disable-annotation', (options = {}) => {
  return (root) => {
    root.walkRules((rule) => {});
  };
});

In the above snippet, we can parse every single css rule. Throw a console.log and see for yourself. As for our example we are only interested in declarations with an annotation. So let's filter the rest out.

JavaScript
index.js
import * as postcss from 'postcss';
 
export default postcss.plugin('postcss-disable-annotation', (options = {}) => {
  return (root) => {
    root.walkRules((rule) => {
      const disable = rule.nodes.filter((x) => {
        return x.type === 'atrule' && x.name === 'disable';
      });
 
      if (disable.length === 1) {
        const color = disable[0].params;
        rule.removeChild(disable[0]);
      }
    });
  };
});

We've kept only the annotated declarations and the ones named 'disable'. We expect to have only one in each selector. If that's the case we're free to delete the @disable #efefef entry and start building the output.

JavaScript
index.js
var postcss = require('postcss');
 
module.exports = postcss.plugin('postcss-disable-annotation', function (opts) {
  opts = opts || {};
 
  return (root) => {
    root.walkRules((rule) => {
      const disable = rule.nodes.filter((x) => {
        return x.type === 'atrule' && x.name === 'disable';
      });
 
      if (disable.length === 1) {
        const color = disable[0].params;
        rule.removeChild(disable[0]);
 
        const new_rule = postcss.rule({
          selector: '&.disabled',
        });
        const decl = postcss.decl({
          prop: 'color',
          value: color,
        });
        new_rule.append(decl);
        rule.append(new_rule);
      }
    });
  };
});

Creating a new rule, we pick the selector's name, and append the color declaration. To wrap things up, place the new rule inside the rule where the annotated declaration used to be. Removing the ES6 features that require Babel, we're free to paste the code in index.js. The output is correct and the crowd goes wild.

CSS
style.css
.link {
  text-decoration: none;
  color: red;
  &.disabled {
    color: #efefef;
  }
}

Well that's it. The PostCSS API docs are must read for any follow up ideas.

PostCSS opens infinite possibilities. I strongly believe that any developer willing to invest some time in this ecosystem, will be rewarded.

Might want to check out my presentation too.

Cheers!