H

August 6, 2015

Lately there has been a ton of buzz regarding PostCSS, a tool for transforming CSS with JS plugins. It consumes a CSS file and splits it into a node tree, then you're free to manipulate the CSS via an API—very cool stuff. What's even better is that this is available via npm and can be integrated into a bunch of task runners.

The other day I was faced with the challenge to limit the precision of all decimal em units in a CSS file to three places. I first thought maybe SASS or Compass could accomplish this, which SASS can but only for numbers that are generated dynamically. I decided to write a PostCSS plugin, because I only have read-access to the CSS and I wanted to quickly see what difference it would make with the limit implemented.

Imagine the CSS file looks like this:

.foo {
  width: 3.33333333333333em;
  height: 7.3456789em;
  padding: .0212em;
}

The Gulpfile that moves the CSS from one directory to another looks like this:

var gulp    = require('gulp');
var notify  = require('gulp-notify');
var plumber = require('gulp-plumber');

gulp.task('css', function() {
  return gulp.src([
      'src/**/*.css'
    ])
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(gulp.dest('build/css'));
});

gulp.task('default', ['css']);

Now we postcss and gulp-postcss to the package.json and run npm install. Once those packages are available we can begin to write the plugin:

// Load the PostCSS module.
var postcss = require('postcss');

// Define the plugin.
var precision = postcss.plugin('postcss-precision', function() {
  var longEmTest = /(\d+)?\.\d{4,}em/gi;

  return function(style) {
    style.eachDecl(function(decl) {


      if (! decl.value || longEmTest.test(decl.value)) {
        // Grab array of matches.
        var matches     = (decl.value + '').match(longEmTest);
        
        // We'll assume there's one.
        var value       = matches[0].substr(0, matches[0].length - 2);
        
        // Round three decimal places.
        var rounded     = Math.round(parseFloat(value) * 1000) / 1000;
        
        // Change the value in the tree.
        decl.value      = decl.value.replace(value, rounded.toString());
      }
    });
  };
});

// Make PostCSS aware of this plugin.
postcss().use(precision);

If you're unsure of how to use the PostCSS API, their documentation is quite helpful.

Now it's time to include this plugin in our Gulp task:

var gulp    = require('gulp');
var notify  = require('gulp-notify');
var plumber = require('gulp-plumber');

// Load the PostCSS module.
var postcss = require('postcss');
var post    = require('gulp-postcss');

// Define the plugin.
var precision = postcss.plugin('postcss-precision', function() {
  var longEmTest = /(\d+)?\.\d{4,}em/gi;

  return function(style) {
    style.eachDecl(function(decl) {


      if (! decl.value || longEmTest.test(decl.value)) {
        // Grab array of matches.
        var matches = (decl.value + '').match(longEmTest);
        
        // We'll assume there's one.
        var value = matches[0].substr(0, matches[0].length - 2);
        
        // Round three decimal places.
        var rounded = Math.round(parseFloat(value) * 1000) / 1000;
        
        // Change the value in the tree.
        decl.value = decl.value.replace(value, rounded.toString());
      }
    });
  };
});

// Make PostCSS aware of this plugin.
postcss().use(precision);

gulp.task('css', function() {
  return gulp.src([
      'src/**/*.css'
    ])
    .pipe(plumber({
      errorHandler: notify.onError('Error: <%= error.message %>')
    }))
    .pipe(post([precision()]))
    .pipe(gulp.dest('build/css'));
});

gulp.task('default', ['css']);

That's a fair chunk of code to consume, but the point is that it's very simple to add a PostCSS plugin to your workflow. In the end, we wind up with the following CSS file:

.foo {
  width: 3.333em;
  height: 7.346em;
  padding: .021em;
}

This might be a simple application of PostCSS, a really sexy application of this is to check if your CSS is compatible with certain browsers and throw errors on properties that may not be fully supported (i.e. doiuse). However you plan to use it, contribute those plugins back to the community!

Next up: Never AutoUpdate™ Anything

Proceed