Breaking up with Sass: it’s not you, it’s me
Right now, I have a near frictionless CSS workflow. I write in Sass, compile with Libsass and get vendor prefixes added with PostCSS/Autoprefixer via Gulp/Grunt. Why would I want to upset that?
Just under a year ago, my interest was piqued by a post by Nicolas Gallagher. In it he described how he/they (Twitter) were using a JS tool to post-process CSS.
More recently, David Clark pinged me about PostCSS and using it instead of Sass. He subsequently wrote on the subject here: http://davidtheclark.com/excited-about-postcss/
David’s excitement re-ignited my curiosity and the thought of using PostCSS for all my CSS needs has been brewing since (PostCSS is the core ‘thing’ that makes other ‘things’ like Autoprefixer possible).
Let’s be clear. There’s nothing ‘wrong’ with Sass. My reasons to consider ending our relationship are mostly selfish. Walk with me:
Everything that can be written in JS will be written in JS
That was Jeff Atwood back in 2007. These words resonate with me even more so now; I don’t think JavaScript is going anywhere soon.
Personally, all my development work is on the front-end of the stack. Currently, my JavaScript skills are ‘beginner’ level. I’d like to consider myself ‘intermediate’ at some point in the future and to get there I need to use JavaScript more frequently.
I know Sass pretty well (I know, you’d hope so given my book on the subject). However, my understanding and continued investment in learning that language is not portable. While many conventions I’ve encountered and now understand are basic Computer Science stuff I wouldn’t have encountered otherwise (as I don’t have a Computer Science background), the actual syntax and its foibles are unique. I can’t apply that knowledge elsewhere.
Furthermore, reliance on Sass, the language, makes me beholden to the features that the authors of Sass add/remove. In many ways this is a good thing. However, I have no C or Ruby chops to extend the language should I need to. For example, I’d really like to see globbing support but one of the authors is adamant that it won’t be part of the language. That frustrates me. I’m not capable enough to change things.
What do I need from Sass
I like to keep my CSS authoring close to CSS. What I mean by that is that I only use Sass features like loops, functions and mixins if I really need to. The biggest wins for me with Sass have always been in terms of variables (for consistency) and partials (organisational). However, there have been occasions where newer features like data structures have really proved useful.
I made a list of the things I need from style sheets (in brackets are what currently facilitates those needs):
- variables [Sass]
- data structures [Sass]
- loops (of data structures) [Sass]
- nesting [Sass]
- color conversions (specifically HSB) [Sass]
- automatic vendor prefixing [PostCSS/Autoprefixer via Gulp]
- ability to group alike media queries (because, why not) [Gulp]
- re-minify CSS (after media queries and Autoprefixer have done their thing) [Gulp]
These days, more and more of my needs are being handled by post-processing and build systems; something that wasn’t the case just a couple of years ago. Back then, my vendor prefixing used to be handled with Compass. My minification used to be handled by Sass. That is no longer the case.
Given these current needs, I wanted to see if it was possible (and preferable) to move the existing Sass based functionality into the PostCSS world.
I’ll document that process now before I attempt to consolidate my current thinking on the matter.
Migrating from Sass to PostCSS
Gulp is my current build tool of choice. I have used Gulp as my ‘passport’ to PostCSS.
Here is the basic gulpfile.js
I used to test PostCSS. The actual part that deals with PostCSS is a small subset of this but I’m listing it all out for the sake of completeness.
var gulp = require('gulp'),
jshint = require('gulp-jshint'),
uglify = require('gulp-uglify'),
imagemin = require('gulp-imagemin'),
rename = require('gulp-rename'),
concat = require('gulp-concat'),
notify = require('gulp-notify'),
cache = require('gulp-cache'),
browserSync = require('browser-sync'),
reload = browserSync.reload;
var gutil = require('gulp-util');
var postcss = require('gulp-postcss');
var simplevars = require('postcss-simple-vars');
var autoprefixer = require('autoprefixer-core');
var mqpacker = require('css-mqpacker');
var csswring = require('csswring');
var nestedcss = require('postcss-nested');
var corepostcss = require('postcss');
var categories = require('./data/cat-colors.json');
var dataloop = function(css) {
for ( var category in categories.colorList ) {
var colorSet = categories.colorList[category];
var borderTop = colorSet[0];
var borderBottom = colorSet[1];
var rule = corepostcss.rule({ selector: '.cat-' + category });
rule.append({ prop: 'border-top', value: '1px solid ' + borderTop});
rule.append({ prop: 'border-bottom', value: '1px solid ' + borderBottom + ";"});
css.append(rule);
}
};
gulp.task('css', function () {
var processors = [
autoprefixer({browsers: ['last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4']}),
simplevars,
nestedcss,
dataloop
];
return gulp.src('./preCSS/*.css')
.pipe(postcss(processors))
.pipe(gulp.dest('./dest'));
});
// Static server
gulp.task('browser-sync', function() {
browserSync({
server: {
baseDir: "./"
}
});
});
// Concatenate & Minify JS
gulp.task('scripts', function() {
return gulp.src('js/*.js')
.pipe(concat('all.js'))
.pipe(gulp.dest('dist'))
.pipe(rename('all.min.js'))
.pipe(uglify())
.pipe(gulp.dest('dist'))
.pipe(reload({stream:true}));
});
// Images
gulp.task('images', function() {
return gulp.src('img/**/*')
.pipe(cache(imagemin({ optimizationLevel: 3, progressive: true, interlaced: true })))
.pipe(gulp.dest('dist/images'))
.pipe(notify({ message: 'Images task complete' }));
});
// Watch
gulp.task('watch', function() {
// Watch .css files
gulp.watch('preCSS/**/*.css', ['css', browserSync.reload]);
// Watch .js files
gulp.watch(['js/**/*.js','main.js'], ['scripts', browserSync.reload]);
// Watch image files
gulp.watch('img/**/*', ['images']);
// Watch any files in dist/, reload on change
gulp.watch("*.html", browserSync.reload);
});
gulp.task('default', ['css', 'browser-sync', 'scripts', 'watch']);
Plugins for Sass like conveniences
I wanted to specifically test nesting, variables and looping through data structures, PostCSS style. By default, PostCSS is pretty lean. You add the additional functionality you need by way of the many and varied plugins.
Therefore PostCSS plugins provide similar variable, nesting and mixin conventions that I am used to with Sass.
These plugins just work. I had to do no work in terms of configuration so I’ll therefore only talk about them briefly.
For example, with the PostCSS Simple Variables plugin, you can use variables just like you would in Sass: $variable-name: value;
. When you want to interpolate, the syntax is slightly different, $(variable-name)_normal-thing
(as opposed to #{$variable-name}
in Sass).
If you want mixins there is PostCSS Mixins. The syntax is again, purposely similar to Sass. I’ve not really done anything with these. I was more interested in data structures as there is nothing ‘off the shelf’ to deal with these.
Data structures and looping
I don’t need to loop through data structures very often but when I do, I’m sure glad there’s a way to do it in Sass. There are a couple of instances in current projects I’m working where I literally would not be without this capability. This was one of the first things I started asking about. And further questioning and a lot of help from Tom (seriously, I couldn’t accomplish anything in JS without Tom) revealed the way to do this in PostCSS.
Unlike Sass, which purposely mixes logic in with styles, PostCSS would ideally like you to keep logic and programming out of your style sheets. I’m torn on this. I can appreciate both approaches. Let’s put the philosophical debates on this aside and merely consider how we can load in a data structure and loop through it in PostCSS land.
Referencing a data structure in PostCSS
The great thing about using PostCSS is that it’s JavaScript. So JSON data structures need no special work to start making use of. You can see in the Gulpfile above I’m merely referencing my test JSON data structure like this:
var categories = require('./data/cat-colors.json');
Here is the content of that JSON file:
{
"colorList": {
"c1" : ["#000000", "#000011"],
"c2" : ["#000011", "#000022"],
"c3" : ["#000022", "#000033"],
"c4" : ["#000033", "#000044"],
"c7" : ["#000044", "#000055"],
"c8" : ["#000055", "#000066"],
"c9" : ["#000066", "#000077"],
"c10" : ["#000077", "#000088"]
}
}
With this data, I want to loop through and choose the first hex value for the border-top colour and the second hex value for the border-bottom colour. I also want to use the key value to make a selector. Here is the loop from the above gulpfile.
var dataloop = function(css) {
for ( var category in categories.colorList ) {
var colorSet = categories.colorList[category];
var borderTop = colorSet[0];
var borderBottom = colorSet[1];
var rule = corepostcss.rule({ selector: '.cat-' + category });
rule.append({ prop: 'border-top', value: '1px solid ' + borderTop});
rule.append({ prop: 'border-bottom', value: '1px solid ' + borderBottom + ";"});
css.append(rule);
}
};
If you are used to writing JavaScript, the above loop will not need explanation. If you don’t currently write much JavaScript, here is the ‘York Notes’ version of what’s going on.
First, we make a variable to serve as a reference to the function:
var dataloop = function(css) {
The argument in there (css)
is what will be passed in from the PostCSS job (the CSS).
Then the loop:
for ( var category in categories.colorList ) {
Remember that we have already referenced our JSON data as ‘categories’. Within that JSON we want the colorList
object, so, using dot notation, that results in categories.colorList
. The var category
is just giving us a dynamic name to serve as the iteration of the loop. Next we want to reference each ‘row’ of that JSON. The next variable serves that purpose:
var colorSet = categories.colorList[category];
With that referenced we can then grab each value inside the array (e.g. colorSet[0]
) to assign the values needed to make the CSS rules we need. The rest of the loop merely deals with creating the rules and appending them to the CSS.
var rule = corepostcss.rule({ selector: '.cat-' + category });
rule.append({ prop: 'border-top', value: '1px solid ' + borderTop});
rule.append({ prop: 'border-bottom', value: '1px solid ' + borderBottom + ";"});
css.append(rule);
var postcss = require('gulp-postcss');
. I therefore ended up pulling in the ‘normal’ PostCSS to give me access to the rule method. If someone knows what I’m doing wrong here, please let me know.
Each time a CSS file is saved, the data is looped through and rules for each key in that data structure are appended to the end of the CSS. For example:
.cat-c1 {
border-top: 1px solid #000000;
border-bottom: 1px solid #000011;
}
The actual PostCSS task
The actual PostCSS task in gulp is nice and simple:
gulp.task('css', function () {
var processors = [
autoprefixer({browsers: ['last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4']}),
simplevars,
nestedcss,
dataloop
];
return gulp.src('./preCSS/*.css')
.pipe(postcss(processors))
.pipe(gulp.dest('./dest'));
});
You can see in the processors
variable you just list out the different plugins you want to process the CSS with. Notice our own dataloop
function is listed here? Any further functions/plugins you wanted to write can be referenced in the same manner. If they live outside the gulpfile you can simply pull them in with a require
.
Subsequently, the capability to do ‘things’ with the CSS is effectively limitless (or limited only by your imagination and capability).
So, to conclude this section, I think it’s safe to say, almost all the issues you would need to solve with Sass could be solved with PostCSS.
Why wouldn’t you use PostCSS?
Like all popular frameworks and tools, there are some good reasons to embrace them. They make it easier to on-board newer developers to your projects. If you can say ‘We use Angular’, or ‘We use Ember’, or ‘We use Sass’ etc it’s easier for new devs to make sense of your work; these are all robust, capable and well-documented projects, regardless of any short-comings.
If you go down this route, to some extent you are creating a bespoke CSS processing tool. That is a double-edged sword.
In addition there is some value being able to include logic in style sheets. The @warn
directive can be pretty handy. Or wrapping sections in @if
statements so they only get output in different eventualities. I have found utility in these features. With Sass they are there, solid, ready and waiting.
Why would you use PostCSS?
PostCSS offers a virtually limitless means of manipulating your CSS in any way you need.
It’s insanely fast. I need to test and substantiate with a huge project but I’d venture at this point it’s faster than Libsass.
If you don’t use JavaScript much, the JavaScript you learn using PostCSS is portable. The knowledge can be applied elsewhere on the stack.
Summary
For the majority of style sheet authors currently (and happily) using Sass, there is little benefit in jumping ship to PostCSS right now. I hope it’s clear from this post that I believe to do so would be for philosophical and/or personal reasons. My default advice to someone looking to pick a tool to help organise and maintain CSS would still be ‘use Sass’.
Furthermore, there are arguments that suggest PostCSS is a better choice than existing pre-processors as it allows you to use the current W3C syntaxes for things like variables/custom properties. The cssnext project takes this approach to its logical conclusion, adding the ability to use a bunch of future (in proposal/dev) CSS module syntaxes today. The thinking being that it therefore provides a more future proof way to author style sheets. While this will appeal to some, I’m not convinced on this approach.
A W3C specification that is not ratified or implemented by more than a couple of vendors is no more stable or future proof that some arbitrary abstraction. If you think otherwise, I’d wonder if you’ve ever tried documenting things like Flexbox or linear-gradients over the years! Plus, changing a variable from one format to another is likely little more than a 10 minute job in any competent text editor.
When it comes to speed, both Libsass and PostCSS are incredibly fast. PostCSS has the edge but whether you’ll see or feel any tangible benefit is questionable; maybe on enormous projects but for most users, probably not. I intend to test this for myself in due course.
You can also facilitate globbing imports with either tool via a build tool like Gulp (the current inlining imports plugin for postCSS doesn’t support globbing) so there’s no inherent advantage to PostCSS in that respect either.
Instead, I think the real attraction with PostCSS is the unshackling of restraints and the portability of skills. Tackling your CSS problems with tools manipulated with JavaScript is either: liberating if you already understand the language well, or time better spent if you have any ambitions to enjoy working in the front (and perhaps even back) of the web stack in the future.
Awesome thoughts Ben. Would be great to see a post-build review of a site built in PostCSS.