Supercharging your CSS with Stylus and PostCSS

Here at Caktus the front-end team stays on the bleeding edge by taking advantage of the latest and greatest tools. We only incorporate features into our packaging that are well-supported and production-ready, as well as those that meet our list of standard browser requirements. Luckily, there are plenty of tools that allow us to use experimental technologies with appropriate fallbacks for non-supported browsers.

Getting Started

Our front-end packaging includes npm and gulp to bundle CSS files differently based on our working environments. It is a good idea to separate local development and production environment pipelines in order to optimize each environment. In our package.json file, we use two scripts: dev and build.

"scripts": {
   "build": "./node_modules/.bin/gulp deploy",
   "dev": "./node_modules/.bin/gulp"
},

Dev is used when the project is run on a local development environment. We use tools like sourcemapping, watchers to track when specified files have changed, and livereload to auto refresh browsers when specific triggers are detected.

Our build script is used for staging and production environments. It is set up to concatenate and minify source files into one CSS file that gets served to the client. Both scripts do a fair amount of preprocessing and postprocessing of our style files and allow us to use some powerful features we would not normally be able to access. I will spend the bulk of this post outlining these features and why they are useful to implement in your next project.

Ways to use Stylus

At Caktus we use Stylus as our CSS preprocessor of choice. It has many of the same features as LESS and SASS; however, the added benefit of Stylus comes from its flexible syntax, ability to run functions, and out-of-the-box custom selectors.

In Stylus, you can structure your style files with more syntactic freedom than other CSS preprocessors. For example, if you prefer a more simplistic approach to writing style rules, you can do so:

body
    font 1rem Helvetica Neue, sans-serif
    margin 0
    padding 0

If you prefer the regular CSS syntax, Stylus supports it. Or, if you prefer any variation in between, Stylus also supports that. With flexible syntax, team members can now determine how to write CSS styles and patterns that work for the team as a whole - which has proven to be helpful for team members who do not come from a front-end background. More importantly, flexible syntax allows us to structure our CSS to be less noisy, which improves clarity and comprehension.

New to CSS preprocessors is Stylus' ability to utilize functions. I find this feature particularly useful when computing values that should not be static but rather relative to other values. In a simple example, we can now set the margin of an element based on a specific formula that is relative to an element's position within a container.

<section>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
 </section>
count = 4
divideByHalf(start, end, val)
    if start > end
       return val
    else
       return divideByHalf(start + 1, end, val/2)

section
    for num in (1..count)
            *:nth-child({num})
                    margin: divideByHalf(1, num, 3.5vw)

Evaluates to:

section *:nth-child(1) {
  margin: 1.75vw;
}
section *:nth-child(2) {
  margin: 0.875vw;
}
section *:nth-child(3) {
  margin: 0.4375vw;
}
section *:nth-child(4) {
  margin: 0.21875vw;
}

Stylus comes with many useful selectors. You can now use partial references and even ranges in partial references to assign an attribute to a nested element without worrying that the parent element will also inherit this attribute.

.menu
    .sub-menu
        display: none

        ^[0]:hover ^[-1..1]
            display: block

Evaluates to:

.menu .sub-menu {
  display: none;
}
.menu:hover .sub-menu {
  display: block;
}

Supercharge with PostCSS

Stylus has a lot of useful functionality and features out of the box, but we can do one better: we can postprocess our style files to be even more robust and future-forward! The main library we use to achieve this is PostCSS.

PostCSS allows us to use a plugin called CSSNext (as well as many other plugins), which in turn enables the use of CSS4 features and autoprefixer. These libraries grant us the luxury of offloading some mental baggage when it comes to writing styles and browser-specific support for all the different browser versions, as well as giving us the freedom to experiment with new technology to make our jobs easier and more sane.

So, what does this look like?

First, we need to set our source files and our environment flag:

var options = {
    stylus: {
        src: './myproject/static/stylus/index.styl',
        watch: './myproject/static/stylus/**/*.styl',
        dest: './myproject/static/css/'
    },
    development: true,
}

Next we create the gulp pipeline:

var stylusTask = function () {
    return gulp.src(options.stylus.src)
        .pipe(stylus())
        .pipe(rename('bundle.css'))
        .pipe(gulp.dest(options.stylus.dest));
};

Nothing too crazy here; we preprocess our style files and combine them into a single file called bundle.css and put it in our specified CSS destination folder.

What if we wanted to minify our CSS file to cut down on file size, but also include a way to debug by referencing the original style file where a rule originates from? We pass in a parameter to Stylus to minify the files and enable sourcemapping:

var stylusTask = function () {
    var stylusOpts = {
        compress: true
    };

    return gulp.src(options.stylus.src)
        .pipe(sourcemaps.init())
        .pipe(stylus(stylusOpts))
        .pipe(sourcemaps.write())
        .pipe(rename('bundle.css'))
        .pipe(gulp.dest(options.stylus.dest));
};

How about integrating some useful plugins that allow us to automatically prefix our styles and allow us to use new technology like CSS Grid, CSS Variables, CSS4 features, etc? We can specify which plugins PostCSS should use for the features we want. In our case, CSSNext includes Autoprefixer, as well as a slew of new features:

var stylusTask = function () {
    var stylusOpts = {
        compress: true
    };

    var plugins = [
        cssnext({browsers: ['last 2 versions']}), // we tell autoprefixer to prefix rules to support the last 2 versions of all browsers
    ];

    return gulp.src(options.stylus.src)
        .pipe(sourcemaps.init())
        .pipe(stylus(stylusOpts))
        .pipe(postcss(plugins))
        .pipe(sourcemaps.write())
        .pipe(rename('bundle.css'))
        .pipe(gulp.dest(options.stylus.dest));
};

What if we want to modify the gulp pipeline in specific, local development only cases? We can use gulpif and lazypipe to pipe in extra tasks conditionally:

var stylusTask = function () {
    var stylusOpts = {
        compress: true
    };
    var plugins = [
        cssnext({browsers: ['last 2 versions']}),
    ];
    var devHelpers = lazypipe()
        .pipe(livereload)
        .pipe(notify, function() {
            console.log('CSS bundle-stylus built in ' + (Date.now() - start) + 'ms');
    });

    return gulp.src(options.stylus.src)
        .pipe(sourcemaps.init())
        .pipe(stylus(stylusOpts))
        .pipe(postcss(plugins))
        .pipe(sourcemaps.write())
        .pipe(rename('bundle.css'))
        .pipe(gulp.dest(options.stylus.dest))
        .pipe(gulpif(options.development, devHelpers()));
  }

Lastly, what if we want to run the gulp pipeline in conjunction with other functions, based on our environment setting? We can achieve this by checking our environment setting variable and running the appropriate commands:

var options = {
  stylus: {
    src: './myproject/static/stylus/index.styl',
    watch: './myproject/static/stylus/**/*.styl',
    dest: './myproject/static/css/'
  },
    development: true,
}

if (argv._ && argv._[0] === 'deploy') {
    options.development = false
} else {
    options.development = true
}

var stylusTask = function () {
    var stylusOpts = {
        compress: true
    };
    var plugins = [
        cssnext({browsers: ['last 2 versions']}),
    ];
    var devHelpers = lazypipe()
        .pipe(livereload)
        .pipe(notify, function() {
        console.log('CSS bundle-stylus built in ' + (Date.now() - start) + 'ms');
    });

    var run = function () {
        return gulp.src(options.stylus.src)
        .pipe(sourcemaps.init())
        .pipe(stylus(stylusOpts))
        .pipe(postcss(plugins))
        .pipe(sourcemaps.write())
        .pipe(rename('bundle.css'))
        .pipe(gulp.dest(options.stylus.dest))
        .pipe(gulpif(options.development, devHelpers()));
    }

    if (options.development) {
        var start = Date.now();
        console.log('Building Stylus bundle');
        stylusOpts.compress = false;
        gulp.watch(options.stylus.watch, run);
        return run()
    }
    else
    {
        return run()
    }
};

gulp.task('css', stylusTask);

gulp.task('rebuild', ['css'])

gulp.task('deploy', ['rebuild']);

Final Thoughts

By customizing our CSS bundling process to take advantage of preprocessing and postprocessing options, we can now claim that our front-end packaging does the following:

  1. Accounts for multiple development environments (local, staging, production) by modularizing the CSS Gulp pipeline task.
  2. Uses style preprocessing that allow us to write style rules using familiar programming paradigms.
  3. Uses style postprocessing to ensure feature support and polyfills for all browsers, and enables us to safely implement experimental technology in production-ready settings.

If you found that helpful, we have more CSS and front-end tips on the blog.

Download Shipping Faster: Django Team Improvements
blog comments powered by Disqus
Times
Check

Success!

Times

You're already subscribed

Times