CSS pre- vs. post-processing

Published on

CSS preprocessors are awesome, the’ve revolutionized CSS authoring. Writing cross-browser CSS today is a lot easier because all vendor prefixes and browser hacks can be abstracted away into mixins, placeholders and what have you. For a while this was more than enough, but because we are obsessive, mentally deranged control-freaks we want more. Always more. MOOOOAAAAR. Say hello to CSS postprocessing!

”Postprocessing”?! I’m so confused!

That’s understandable, I don’t think “postprocessing” is really an official term, but this is how I would explain it---CSS preprocessors (Sass, Less etc.) parse and compile a CSS extension language into plain ol’ CSS, while postprocessors parse and process plain ol’ CSS. That was terrible, sorry, but if you didn’t get it, it should become clear eventually.

There are many CSS postprocessors out there, for example Autoprefixer (see it in action online!), PostCSS and rework.

Why would I use a postprocessor?

Well, let’s take Autoprefixer as an example, it parses your CSS and adds vendor prefixes using the database from Can I Use. Imagine you’re writing Sass and you want to apply a CSS3 rule which is not well supported when unprefixed, so naturally you’d use a mixin:

// ...a LOT of Sass
 
@mixin box-sizing($value) {
  -webkit-box-sizing: $value;
     -moz-box-sizing: $value;
          box-sizing: $value;
}
 
.box {
  @include box-sizing(border-box);
}
 
// out of my way, Sassing through...

Why is Autoprefixer better? Because you can just do this:

.box {
  box-sizing: border-box;
}

Write your Sass/Less/Stylus/CSS without a care in the world, because the compiled CSS will get parsed and necessary vendor prefixes will be added:

.box {
  -webkit-box-sizing: border-box;
     -moz-box-sizing: border-box;
          box-sizing: border-box;
}

There are at least 4 benefits of this approach:

  1. You don’t have to think about whether the current rule needs to be prefixed or not, i.e. whether you should use a mixin.

  2. You can later change the browsers you want to support just by configuring Autoprefixer. More on that below.

  3. If you happen to be working on a non-prefixed codebase, you don’t have to locate every CSS3 declaration and replace it with a mixin.

  4. It’s faster than Sass.

Autoprefixer has an excellent support for just about any build tool you might be using. For my further examples I’ll be using Gulp (sorry, non-Gulpers), so your task might look like this:

var sass = require('gulp-ruby-sass');
var autoprefixer = require('gulp-autoprefixer');
 
gulp.task('styles', function() {
  return gulp.src('app/styles/main.scss')
    .pipe(sass())
    .pipe(autoprefixer({browsers: ['last 3 versions']}))
    .pipe(gulp.dest('dist/styles'));
});

I told Autoprefixer to add vendor prefixes for last 3 versions of every browser. :sunglasses:

The next level

You’ll notice that Autoprefixer doesn’t do browser hacks, it only adds vendor prefixes. This is where PostCSS comes in.

Imagine you want to use opacity like a decent human being:

.box {
  opacity: 0.5;
}

But IE 8 says “What is this opacity thingy? Is that something you eat?”, so you have to hack it a bit. Are you seriously thinking about using a mixin just because you want to add a hack for a single browser that will soon be irrelevant? We can instead write a custom PostCSS processor:

function(css) {
  css.eachDecl(function(decl, i) {
    if (decl.prop === 'opacity') {
      decl.parent.insertAfter(i, {
        prop: '-ms-filter',
        value: '"progid:DXImageTransform.Microsoft.Alpha(Opacity=' + (parseFloat(decl.value) * 100) + ')"'
      });
    }
  });
}

This loops through each declaration and if the property is opacity it will insert the IE 8 opacity hack after it. Now we need to plug it in our Gulp task:

var sass = require('gulp-ruby-sass');
var autoprefixer = require('gulp-autoprefixer');
var postcss = require('gulp-postcss');
var opacity = function(css) {
  css.eachDecl(function(decl, i) {
    if (decl.prop === 'opacity') {
      decl.parent.insertAfter(i, {
        prop: '-ms-filter',
        value: '"progid:DXImageTransform.Microsoft.Alpha(Opacity=' + (parseFloat(decl.value) * 100) + ')"'
      });
    }
  });
};
 
gulp.task('styles', function() {
  return gulp.src('app/styles/main.scss')
    .pipe(sass())
    .pipe(autoprefixer({browsers: ['last 3 versions']}))
    .pipe(postcss([opacity]))
    .pipe(gulp.dest('dist/styles'));
});

If you want to be cool, you can add Autoprefixer as a PostCSS processor instead of a Gulp plugin, so instead of gulp-autoprefixer you should require autoprefixer-core:

var sass = require('gulp-ruby-sass');
var autoprefixer = require('autoprefixer-core');
var postcss = require('gulp-postcss');
var opacity = function(css) {
  css.eachDecl(function(decl, i) {
    if (decl.prop === 'opacity') {
      decl.parent.insertAfter(i, {
        prop: '-ms-filter',
        value: '"progid:DXImageTransform.Microsoft.Alpha(Opacity=' + (parseFloat(decl.value) * 100) + ')"'
      });
    }
  });
};
 
gulp.task('styles', function() {
  return gulp.src('app/styles/main.scss')
    .pipe(sass())
    .pipe(postcss([
      autoprefixer({browsers: ['last 3 versions']}),
      opacity
    ]))
    .pipe(gulp.dest('dist/styles'));
});

Now after the task runs we will end up with the following rule:

.box {
  opacity: 0.5;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
}

Hideous… but exactly what we wanted. Now when we decide we don’t care about IE 8 anymore, we can just plug out our processor from the Gulp task.

So when do we use CSS pre- vs. post-processors?

Avoid using preprocessors for cross-browser fixes like vendor prefixes and polyfills, let postprocessors deal with those. You can use preprocessors for things that are more fun, like color schemes, font-face mixins, layout mixins etc.

As for postprocessors, you don’t have to limit them to cross-browser fixes. Check out the list of existing PostCSS processors to get an idea of what you can do, because we seriously just scratched the surface here.

I’m really excited about this, how about you?