Modular CSS preprocessing with rework
Several months ago I started a project named Rework, a very fast, simple, flexible, and modular CSS preprocessor. The biggest and most obvious question I get is how this tool compares to something like Stylus, LESS, or Sass, and why would you want to use it.
The simple answer is that Rework caters to a different audience, and provide you the flexibility to craft the preprocessor you want, not the choices the author(s) have force on you.
Our goal with Rework contrasts the others, as it does not provide a higher level language within the CSS documents, Rework’s goal is to make those constructs unnecessary, being the most transparent CSS pre-processor out there.
Speed
Rework was designed to be simple and fast out of the box, without implementing complex caching models allowing everyone to hack on Rework itself or associate plugins easily.
Powering rework is a css parser written in JavaScript that may be used with node.js or in the browser. On the other end there is the css compiler which accepts the AST from css-parse, outputting CSS.
So where does Rework come in? Rework is the plugin and transformation engine that sits in-between these two tools. The AST is manipulated by Rework and its plugins (or user-defined plugins), which is then passed to the compiler.
Because these transformations are performed in JavaScript and not in a specialized preprocessor language they are simple to implement and efficient.
Let’s check out what it can do!
Modularity & Flexibility
Before diving into the plugins bundled with Rework, let’s take a look at how they can be applied. Rework’s API is very simple, you can specify vendor prefixes with .vendors(), which all plugins have access to, or you may pass specific vendor prefixes to most plugins themselves. Plugins are passed to a .use() method, and then finally the CSS output is returned with .toString(). In practice this looks something like:
var rework = require('rework');
var css = rework('string of css here')
.vendors(['-webkit-', '-moz-'])
.use(rework.prefixValue('linear-gradient'))
.use(rework.prefixValue('radial-gradient'))
.use(rework.prefixValue('transform'))
.use(rework.vars())
.use(rework.colors())
.toString()
Out of the box rework provides plugins for the following, which may be mixed and matched as you wish:
- media macros — define your own @media queries
- ease — several additional easing functions
- at2x — serve high resolution images
- prefix — add vendor prefixes to properties
- prefixValue — add vendor prefixes to values
- prefixSelectors — add prefixes to selectors
- opacity — add IE opacity support
- url — rewrite
url()s with a callback function
- vars — add css variable support
- keyframes — add @keyframe vendor prefixing
- colors — add colour helpers like
rgba(#fc0, .5)
- references — add property references support
height: @width etc
- mixin — add custom property logic with mixing
- extend — add
extend: selector support
Let’s look at some examples!
Vendor prefixes
The prefix, prefixValue, and keyframes plugins make vendor prefixing extremely simple. Here the prefix plugin is applied to only two properties for illustration:
var rework = require('..')
var read = require('fs').readFileSync
var css = rework(read('examples/prefix.css', 'utf8'))
.vendors(['-webkit-', '-moz-'])
.use(rework.prefix('border-radius'))
.use(rework.prefix('box-shadow'))
.toString()
console.log(css)
With the prefix.css file containing the following CSS:
.button {
border-radius: 5px;
box-shadow: inset 0 0 1px white;
}
Rework yields the following prefixed output:
.button {
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
-webkit-box-shadow: inset 0 0 1px white;
-moz-box-shadow: inset 0 0 1px white;
box-shadow: inset 0 0 1px white
}
Extending
The extend plugin adds support for a special property named extend, which propagates the selector back up the AST so that the styles are applied without duplication. For example here’s a contrived green join button implementation:
button {
padding: 5px 10px;
border: 1px solid #eee;
border-bottom-color: #ddd;
}
.green {
background: green;
padding: 10px 15px
}
a.join {
extend: button;
extend: .green;
padding: 20px;
}
With this plugin enabled both of the extend properties are gone, and you’re left with the CSS you might expect:
button,
a.join {
padding: 5px 10px;
border: 1px solid #eee;
border-bottom-color: #ddd
}
.green,
a.join {
background: green;
padding: 10px 15px
}
a.join {
padding: 20px
}
Better rgba()
Ever had a color hex and then wanted to alter the alpha channel? Then you probably know how annoying this is with CSS. The colors plugin allows you to pass hex values to rgba(), for example rgba(#c00, .5) which is then compiled to rgba(204,0,0,0.5).
Writing your own plugins
Writing plugins for Rework is simple, suppose for example you want the ability to create your own properties, here expanding single-line position properties:
#logo {
absolute: top left;
}
#logo {
relative: top 5px left;
}
#logo {
fixed: top 5px left 10px;
}
Into the valid CSS shown below:
#logo {
position: absolute;
top: 0;
left: 0
}
#logo {
position: relative;
top: 5px;
left: 0
}
#logo {
position: fixed;
top: 5px;
left: 10px
}
The AST css-parse provides you is made up of simple objects and arrays, making it extremely easy to manipulate without extra knowledge of how the nodes are constructed or strange internal APIs. In the following plugin function we walk the declarations, check if the property is one of the positions, parse the value by splitting on whitespace and then inject the new css properties.
function positions() {
var positions = ['absolute', 'relative', 'fixed'];
return function(style){
rework.visit.declarations(style, function(declarations){
declarations.forEach(function(decl, i){
if (!~positions.indexOf(decl.property)) return;
var args = decl.value.split(/\s+/);
var arg, n;
// remove original
declarations.splice(i, 1);
// position prop
declarations.push({
property: 'position',
value: decl.property
});
// position
while (args.length) {
arg = args.shift();
n = parseFloat(args[0]) ? args.shift() : 0;
declarations.push({
property: arg,
value: n
});
}
});
});
}
}
To use this plugin you would simply pass it to .use():
var css = rework('#logo { absolute: top left; }')
.use(positions())
.toString()
Significant whitespace
Since CSS has a relatively simple grammar significant whitespace can help improve your productivity without turning the whole thing into an unreadable mess, this is especially true since Rework does not provide the higher level language constructs, which can get lost in the indentation. This is where css-whitespace comes in! It lets you write things like:
ul
li
background: #eee
&:hover
background: white
&:active
background: #ddd
Which yields:
ul li:hover {
background: white;
}
ul li:active {
background: #ddd;
}
ul li {
background: #eee;
}
Future
In the near future we’ll be providing a more intuitive way to apply all or large chunks of the functionality Rework provides without manually adding all of the plugins, as well as improving the rework(1) executable.
Stylus gets @extend
@extend is a great little feature which originated in SASS, and now finds a home in Stylus. If you’ve ever written CSS like this, you know it can become quite the pain to maintain large lists of selectors:
.message,
.warning,
.error {
font-size: 14px;
padding: 5px 10px;
border: 1px solid #eee;
border-radius: 5px;
}
.warning {
color: yellow;
}
.error {
color: red;
}
@extend makes this process flow naturally, and even supports inheritance. In Stylus you may use @extend or @extends, I prefer the latter personally but that’s up to you! Using this feature you would define .message on it’s own, and simply __@extend_ it from within the other rulesets:
.message {
font-size: 14px;
padding: 5px 10px;
border: 1px solid #eee;
border-radius: 5px;
}
.warning {
@extends .message;
color: yellow;
}
.error {
@extends .message;
color: red;
}
Taking this even further with inheritance:
.fatal-error {
@extends .error;
font-weight: bold
}
Would yield the following CSS:
.message,
.warning,
.error,
.fatal-error {
font-size: 14px;
padding: 5px 10px;
border: 1px solid #eee;
border-radius: 5px;
}
.warning {
color: #ff0;
}
.error,
.fatal-error {
color: #f00;
}
.fatal-error {
font-weight: bold;
}
Here’s another example:
form
button
padding: 10px
border: 1px solid #eee
border-radius: 5px
ul
li a
@extends form button
Yielding:
form button,
ul li a {
padding: 10px;
border: 1px solid #eee;
border-radius: 5px;
}
And of course you may utilize the alternative Stylus syntax if you prefer:
.message
font-size: 14px
padding: 5px 10px
border: 1px solid #eee
border-radius: 5px
.warning
@extends .message
color: yellow
.error
@extends .message
color: red
.fatal-error
@extends .error
font-weight: bold
Grab Stylus 0.22.4 for these goodies!
This short Vimeo screencast illustrates why Stylus is the most like CSS as a stylesheet “preprocessor”. More on Stylus here.
Stylus 0.15.1 - new property reference & @keyframes fabrication features
Stylus 0.15.x adds a bunch of cool new functionality as well as some important bug fixes.
@keyframes fabrication
The new keyframes fabrication support automatically duplicates your @keyframes definitions to support vendor prefixes. Note that this is only a default, as shown below:
@-moz-keyframes foo {
0% {
color: #000;
}
100% {
color: #fff;
}
}
@-webkit-keyframes foo {
0% {
color: #000;
}
100% {
color: #fff;
}
}
@keyframes foo {
0% {
color: #000;
}
100% {
color: #fff;
}
}
one can tweak this functionality using the vendors list as shown in the following snippet which will only expand to the -webkit- vendor prefix.
vendors = moz official
@keyframes foo {
from {
color: black
}
to {
color: white
}
}
The vendors property may be utilized more in the future, and perhaps in frameworks like “nib” to unify configuration.
Property reference
The syntax @<property> has been introduced to reference values of existing properties. For example if you want to re-use values, you may typically do something like below instead of typing the value several times, also allowing you to use functions with w.
.button
width: w = 50px
height: w
The new property reference syntax allows you to treat properties as variables, which can also be accessed within mixins:
.button
width: 50px
height: @width
Comment compilation
When the compress option is true Stylus will not output single-line nor multi-line comments, which are commonly found used for licensing in stylesheet frameworks etc, so it’s useful to strip these out however you can now force output with the ! character /*!.
New built-in functions
The math functions cos() and sin() were added, as well as exposing PI.
Bug fixes
Stylus vs SASS vs LESS error reporting
I was curious what SASS did as far as error reporting goes, so I tried the following snippet with SASS, LESS as well as Stylus.
body {
form input {
background: foo[fail];
}
}
LESS
The output from LESS was terrible:
Syntax Error on line 4
note: that for LESS I had to tweak the input to “foo[fail’]’;” since it simply consumes the input above
SASS
The SASS output was not bad, it shows you a tiny chunk of the line for context:
Syntax error: Invalid CSS after "...background: foo": expected ";", was "[fail];"
on line 4 of standard input
Use --trace for backtrace.
note: —trace is for the ruby stack trace, not SASS
Stylus
Then we have Stylus, showing you ~8 lines of context (by default), and even a detailed stack trace including the call sites much like you would find in other languages.
Error: /tmp/stylus/test.styl:4
1|
2| body {
3| form input {
> 4| background: foo[fail];
5| }
6| }
7|
cannot perform foo[(fail)]
at "form input" (/tmp/stylus/test.styl:4)
at "body" (/tmp/stylus/test.styl:3)
Extend Sylus with a Nib
Today I am releasing Nib, an extensions library for Stylus, inspired by the SASS compass library. Nib is a very new library with a minimal feature set at the moment, however I need to get some of my semi-finished projects open-sourced to make way for bigger badder things, and I’m already finding this project quite useful so feedback and contributions are always welcome.
If you dont like to read, feel free to check out the short screencast.
Vendor Support
Nib currently supplies some cross-browser vendor support for properties, however this will surely expand and be refined as we go.
Enjoy The Little Things
Ever forget if nowrap is no-wrap, or whitespace over white-space? well I do, thanks to simple definitions like the following, these are completely interchangeable:
no-wrap = unquote('nowrap')
Augmented Border Radius
Border radius works as you might expected, however it is also augmented to support positioning. For example to round only the top of a box, use top 5px, or perhaps round the top and bottom differently with top 5px, buttom 10px. For example:
button {
border-radius: top left 5px, bottom right 10px;
}
yields:
button {
-moz-border-radius-topleft: 5px;
-webkit-border-top-left-radius: 5px;
border-top-left-radius: 5px;
-moz-border-radius-bottomright: 10px;
-webkit-border-bottom-right-radius: 10px;
border-bottom-right-radius: 10px;
}
Position Properties
Nib has three position mixins or “custom properties”, fixed, absolute, and relative. These shorthands are helpful since you can define three properties in one concise, legible manner.
fixed: top left
fixed: top 5px left
fixed: top left 5px
absolute: top 5px left 5px
relative: left -5px
Expanding to:
position: fixed;
top: 0;
left: 0;
etc.
Gradients
Nib makes working with gradients easier than you could imagine. The following call to linear-gradient() duplicates the property to which it is assigned to (not bound only to “background”), and generates the appropriate webkit and mozilla output:
body
background: linear-gradient(right bottom, white, 80% black)
yields:
body {
background: -webkit-gradient(linear, right bottom, left top, color-stop(0, #fff), color-stop(0.8, #000));
background: -moz-linear-gradient(right bottom, #fff 0%, #000 80%);
background: linear-gradient(right bottom, #fff 0%, #000 80%);
}
You may pass as many color stops as you like, optionally providing a position before or after the color for each, whichever you prefer.
Gradient Image Generation
Another powerful feature of Nib, is the ability to utilize node-canvas when installed, and auto-generate a data URI representation of the gradient for further browser support. The image below displays google chrome’s render on the left, and node-canvas on the right.

The linear-gradient-image() function generates the data uri only:
body
background: linear-gradient-image(50px top, white, black);
Alternatively we can pass the size to a regular call to linear-gradient() and the data uri will be created along with the other properties:
body
background: linear-gradient(50px top, white, black);
Components
Nib currently ships with 5 or 6 sets of buttons, however I welcome the addition of other flexible components, and of course more buttons :). Below is an example of utilizing the “bold” button, assigning a glow which adjusts appropriately within the mixin.
.bold
bold-button()
.bold-alternate
bold-button(glow: #00ABFA)
