Custom CSS preprocessing

Did you know that you can build your own CSS preprocessor with Node.js libraries? They can be used alongside established preprocessors like Sass, and are useful for defining tasks beyond preprocessing.

Libraries like Rework and PostCSS let you create and assemble an arbitrary collection of plugins that can inspect or manipulate CSS.

At the time of writing, Twitter uses Rework to perform various tasks against our CSS source code for twitter.com.

Creating a CSS preprocessor with Rework

At its core, Rework is a module that accepts a string of CSS, produces a CSS abstract syntax tree (AST), and provides an API for manipulating that AST. Plugins are functions that have access to the AST and a Rework instance. Rework lets you chain together different plugins and generate a string of new CSS when you’re done.

The source string is passed into the rework function and each plugin is applied with .use(fn). The plugins transform the data in the AST, and .toString() generates the new string of CSS.

Below is an example of a custom preprocessor script using Rework and Autoprefixer. It’s a simplified version of the transformation step we use for twitter.com’s CSS.

var autoprefixer = require('autoprefixer');
var calc = require('rework-calc');
var rework = require('rework');
var vars = require('rework-vars')();

var css = fs.readFileSync('./css/main.css', 'utf-8');

css = rework(css)
  .use(vars)
  .use(calc)
  .toString();

css = autoprefixer().process(css);

fs.writeFileSync('./build/bundle.css', css)

The script runs rework-vars, rework-calc, and then passes the CSS to Autoprefixer (which uses PostCSS internally) to handle the addition of any necessary vendor prefixes.

rework-vars provides a limited subset of the features described in the W3C-style CSS custom property spec. It’s not a polyfill!

Variables can be declared as custom CSS properties on the :root element, prefixed with --. Variables are referenced with the var() function, taking the name of a variable as the first argument and an optional fallback as the second.

For example, this source:

:root {
  --width-button: 200px;
}

.button {
  width: var(--width-button);
}

yields:

.button {
  width: 200px;
}

There are many different Rework plugins that you can use to create a custom preprocessor. A more complete list is available on npm. In order to limit the chances of long-term divergence between our source code and native CSS, I’ve chosen to stick fairly closely to features that are aligned with future additions to native CSS.

Creating your own Rework plugin

Rework plugins are functions that inspect or mutate the AST they are provided. Below is a plugin that rewrites the value of any font-family property to sans-serif.

module.exports = function plugin(ast, reworkInstance) {
  ast.rules.forEach(function (rule) {
    if (rule.type != 'rule') return;

    rule.declarations.forEach(function (declaration, index) {
      if (declaration.property == 'font-family') {
        declaration.value = 'sans-serif';
      }
    });
  });
};

Rework uses css-parse to create the AST. Unfortunately, both projects are currently lacking comprehensive documentation of the AST, but it’s not difficult to piece it together yourself.

Beyond preprocessing

Since Rework and PostCSS expose an AST and provide a plugin API, they can be used for other CSS tasks, not just preprocessing.

At Twitter, our CSS build pipeline allows you to perform custom tasks at 2 stages of the process: on individual files and on generated bundles. We use Rework at both stages.

Individual files are tested with rework-suit-conformance to ensure that the SUIT-style CSS for a component is properly scoped.

/** @define MyComponent */

:root {
  --property-MyComponent: value;
}

.MyComponent {}

Bundles are preprocessed as previously described, and also tested with rework-ie-limits to ensure that the number of selectors doesn’t exceed IE 8/9’s limit of 4095 selectors per style sheet.

Other tasks you can perform include generating RTL style sheets (e.g., css-flip) and extracting detailed information about the perceived health of your CSS (e.g., the number of different colours used, duplicate selectors, etc.).

Hopefully this has given you a small glimpse into some of the benefits and flexibility of using these tools to work with CSS.

Reply on Twitter Retweet on Twitter Favorite on Twitter