Introduction

Roole is a language that compiles to CSS.

It drew many inspirations from other CSS preprocessing languages like Sass, LESS and Stylus.

The most unique feature of Roole is that it has vendor prefixing built-in, so the language stays dead simple yet being able to prefix some extremely complex rules transparently. Since Roole is also a superset of CSS, you can use it directly as a CSS prefixer.

Roole is implemented in JavaScript, so it can be run both on the server side (via node.js) or in a browser (run unit tests to check if Roole works in your browser).

Overview

Roole is a superset of CSS:

body { margin: 0 }
body {
margin: 0;
}

Store repeating values in variables:

$position = left;

#sidebar {
float: $position;
margin-$position: 20px;
}
#sidebar {
float: left;
margin-left: 20px;
}

Conditonally generate rules:

$support-old-ie = false;

li {
@if $support-old-ie {
display: inline;
}
float: left;
margin-left: 10px;
}
li {
float: left;
margin-left: 10px;
}

Quickly generate many rules:

@for $i in 1..3 {
.span-$i {
width: $i * 60px;
}
}
.span-1 {
width: 60px;
}

.span-2 {
width: 120px;
}

.span-3 {
width: 180px;
}

Define your own way of generating rules:

$button = @function $color, $bg-color {
display: inline-block;
color: $color;
background-color: $bg-color;
};

.submit {
@mixin $button(black, white);
}

.reset {
@mixin $button(red, white);
}
.submit {
display: inline-block;
color: black;
background-color: white;
}

.reset {
display: inline-block;
color: red;
background-color: white;
}

Or simply extend already defined rules:

.button {
display: inline-block;
color: black;
background-color: white;
}

.submit {
@extend .button;
}

.reset {
@extend .button;
color: red;
}
.button,
.submit,
.reset {
display: inline-block;
color: black;
background-color: white;
}

.reset {
color: red;
}

And forget about prefixing:

@keyframes become-round {
from {
border-radius: 0;
}
to {
border-radius: 50%;
}
}
@-webkit-keyframes become-round {
from {
-webkit-border-radius: 0;
border-radius: 0;
}
to {
-webkit-border-radius: 50%;
border-radius: 50%;
}
}

@-moz-keyframes become-round {
from {
-moz-border-radius: 0;
border-radius: 0;
}
to {
-moz-border-radius: 50%;
border-radius: 50%;
}
}

@-o-keyframes become-round {
from {
border-radius: 0;
}
to {
border-radius: 50%;
}
}

@keyframes become-round {
from {
border-radius: 0;
}
to {
border-radius: 50%;
}
}

Installation

Node.js

Run command:

npm install roole -g

Browser

Insert the downloaded file into HTML:

<script src="/path/to/roole.js"></script>

Usage

Command line

Compile a single file:

roole /path/to/style.roo

For more usage on the roole command, please run roole -h.

Browser

Link to an external file:

<link rel="stylesheet/roole" href="styles.roo">

Or embed code directly:

<style type="text/roole">
// put code here
</style>

JavaScript API

roole.compile(input, options, callback)

Language

Rule Set

Rule sets can be nested, and their selectors will be joined together:

#header {
.logo {
float: left;
}
}
#header .logo {
float: left;
}
ul {
overflow: hidden;

> li {
float: left;
}
}
ul {
overflow: hidden;
}
ul > li {
float: left;
}
#main, #sidebar {
h1, h2 {
color: #333;
}
}
#main h1,
#main h2,
#sidebar h1,
#sidebar h2 {
color: #333;
}

Use the & selector to reference the outer rule set's selector:

a {
&:hover {
text-decoration: underline;
}
}
a:hover {
text-decoration: underline;
}
img {
a & {
border: none;
}
}
a img {
border: none;
}

The & selector can also be nested:

a {
&:hover {
text-decoration: underline;
.button & {
text-decoration: none;
}
}
}
a:hover {
text-decoration: underline;
}
.button a:hover {
text-decoration: none;
}

@media

@media can be nested into rule sets:

#sidebar {
@media print {
display: none;
}
}
@media print {
#sidebar {
display: none;
}
}
#container {
@media print {
.sidebar {
display: none;
}
}
}
@media print {
#container .sidebar {
display: none;
}
}

Or within one another, and their media queries will be joined together:

@media screen {
a {
color: blue;

@media (monochrome) {
color: black;
}
}
}
@media screen {
a {
color: blue;
}
}
@media screen and (monochrome) {
a {
color: black;
}
}

Comment

Both single-line commnets // and multi-line commnets /* */ are supported:

/*
* Box module
*
* Display a nice box
*/

.box {
float: left;
margin-left: 20px;
// Fix IE6
display: inline;
}
/*
* Box module
*
* Display a nice box
*/

.box {
float: left;
margin-left: 20px;
display: inline;
}

Currently only the first top-level multi-line comment will be preserved in the generated CSS, all other comments are discarded.

Variable

Variables are case-sensitive. Their names start with $ and are defined using assignments:

$margin = 20px 0;
$MARGIN = 30px 0;

p {
margin: $margin;
}
p {
margin: 20px 0;
}

If variables are assigned using ?=, the assigments will only success if the variables is currently undefined:

$margin = 20px 0;
$margin ?= 30px 0;

p {
margin: $margin;
}
p {
margin: 20px 0;
}
$margin ?= 0 20px;

p {
margin: $margin;
}
p {
margin: 0 20px;
}

Variables are allowed where values like numbers, strings, identifiers, etc are allowed:

$tag = body;

$tag {
margin: 0;
}
body {
margin: 0;
}
$attribute = type;
$value = button;

input[$attribute=$value] {
border: none;
}
input[type=button] {
border: none;
}
$property = margin;
$value = 20px;

p {
$property: $value;
}
p {
margin: 20px;
}
$feature = max-width;
$value = 1024px;

@media ($feature: $value) {
#main {
width: 960px;
}
}
@media (max-width: 1024px) {
#main {
width: 960px;
}
}

When being assigned with an string value, variables can also be used as selectors:

$selector = '#sidebar a';

$selector {
color: green;
}
#sidebar a {
color: green;
}
$tab = '.tabs .tab';

#sidebar $tab {
padding: 5px;
}
#sidebar .tabs .tab {
padding: 5px;
}

This also works for media queries:

$query = '(max-width: 1024px)';

@media $query {
body {
width: 960px;
}
}
@media (max-width: 1024px) {
body {
width: 960px;
}
}
$lt-desktop = '(max-width: 979px)';
$gt-phone = '(min-width: 768px)';

@media $gt-phone and $lt-desktop {
body {
width: 960px;
}
}
@media (min-width: 768px) and (max-width: 979px) {
body {
width: 960px;
}
}

Variables can also be used in interpolations (see the next section).

Interpolation

Variables can be interpolated into doubled-quoted strings:

$number = 12;

.heading::before {
content: "Chapter $number: ";
}
.heading::before {
content: "Chapter 12: ";
}

But not single-quoted strings:

.heading::before {
content: 'Chapter $num: ';
}
.heading::before {
content: 'Chapter $num: ';
}

Variables can also be interpolated into identifiers:

$name = star;

.icon-$name {
width: 20px;
height: 20px;
}
.icon-star {
width: 20px;
height: 20px;
}
$position = left;

.sidebar {
padding-$position: 20px;
border-$position: 1px solid;
}
.sidebar {
padding-left: 20px;
border-left: 1px solid;
}

Use \$ to escape variables inside strings:

.heading::before {
content: "Chapter \$number: ";
}
.heading::before {
content: "Chapter \$number: ";
}

Wrap the variable in curly braces {} to seperate characters come after it, which would otherwise be part of its name:

$chapter = 4;

.figcaption::before {
content: "Figure {$chapter}-12: ";
}
.figcaption::before {
content: "Figure 4-12: ";
}
$position = left;

.sidebar {
border-{$position}-width: 1px;
}
.sidebar {
border-left-width: 1px;
}

Use \{ to escape it:

$chapter = 4;

.figcaption::before {
content: "Figure \{$chapter}-12: ";
}
.figcaption::before {
content: "Figure \{4}-12: ";
}

If the curly brace does not form an interpolation there is no need to escape it:

.figcaption::before {
content: "Figure {\$chapter}-12: ";
}
.figcaption::before {
content: "Figure {\$chapter}-12: ";
}
.title::before {
content: "latex \\hat{x}";
}
.title::before {
content: "latex \\hat{x}";
}

Operation

Arithmetic operators +, -, *, / and parentheses () are supported:

$total = 250px;
$padding = 20px;
$border = 1px;

#sidebar {
width: $total - ($padding + $border) * 2;
padding: 0 $padding;
border-width: $border;
}
#sidebar {
width: 208px;
padding: 0 20px;
border-width: 1px;
}

At lease one space should exist around /, otherwise it is generated literally:

body {
font: 14px/1.25 sans-serif;
}
body {
font: 14px/1.25 sans-serif;
}
@media (device-aspect-ratio: 16/9) {
body {
background: url(bg-16-9.png);
}
}
@media (device-aspect-ratio: 16/9) {
body {
background: url(bg-16-9.png);
}
}

At lease one space should exist on the right side of +(-), or no space exists on both side, otherwise unary +(-) is applied:

#box {
margin: 40px -20px;
}
#box {
margin: 40px -20px;
}
#box {
margin: 40px +20px;
}
#box {
margin: 40px 20px;
}

Arithmetic operations can be combined with assignments:

$text = 'Hello, ';

.guest::before {
$text += 'Guest';
content: $text;
}
.guest::before {
content: 'Hello, Guest';
}

Comparison operation are also supported, which comes in handy when specifying @if conditions (see the next section).

@if

@if allows rules inside it to be conditionally generated:

$support-old-ie = false;

li {
@if $support-old-ie {
display: inline;
}
float: left;
margin-left: 10px;
}
li {
float: left;
margin-left: 10px;
}

Sample truthy values: true, 12, 0.5em, '0'. Sample falsey values: false, 0, 0px, "".


@if can be followed by any number of @else if and optionally one @else:

$color = black;

body {
@if $color is white {
background: #fff;
} @else if $color is black {
background: #000;
} @else if $color is gray {
background: #999;
} @else {
background: url(bg.png);
}
}
body {
background: #000;
}

This example also demonstrates the use of is operator.


Like is, which tests equalitys, isnt tests inequality:

$size = large;

.button {
@if $size isnt small {
border: 1px solid;
}
}
.button {
border: 1px solid;
}

and expects values on its both sides to be truthy:

$size = large;
$type = split;

.button {
@if $size is large and $type is split {
padding: 10px;
}
}
.button {
padding: 10px;
}

or expects values on its either sides to be truthy:

$size = large;

.button {
@if $size is medium or $size is large {
border: 1px solid;
}
}
.button {
border: 1px solid;
}

<, <=, > and >= compare numeric values:

$width = 100px;

.button {
@if $width < 100px {
border: none;
} @else if $width >= 100px and $width < 200px {
border: 1px solid;
}
}
.button {
border: 1px solid;
}

Sample numberic values: 1.2, 2em, 50%.

@for

@for allows rules in it to be generated multiple times:

@for $i in 1..3 {
.span-$i {
width: $i * 60px;
}
}
.span-1 {
width: 60px;
}

.span-2 {
width: 120px;
}

.span-3 {
width: 180px;
}

Values like 1..3 are ranges, and are inclusive. Use ... to denote an exclusive range:

@for $i in 1...3 {
.span-$i {
width: $i * 60px;
}
}
.span-1 {
width: 60px;
}

.span-2 {
width: 120px;
}

Ranges can be in reversed order:

@for $i in 3..1 {
.span-$i {
width: $i * 60px;
}
}
.span-3 {
width: 180px;
}

.span-2 {
width: 120px;
}

.span-1 {
width: 60px;
}
@for $i in 3...1 {
.span-$i {
width: $i * 60px;
}
}
.span-3 {
width: 180px;
}

.span-2 {
width: 120px;
}

Ranges are essentially lists, so regular lists works as well:

@for $icon in arrow star heart {
.icon-$icon {
background: url("$icon.png");
}
}
.icon-arrow {
background: url("arrow.png");
}

.icon-star {
background: url("star.png");
}

.icon-heart {
background: url("heart.png");
}

Lists are values separated by spaces, commas ,, or slashs /.


To specify a step other than 1, use by:

@for $i by 2 in 1..5 {
.span-$i {
width: $i * 60px;
}
}
.span-1 {
width: 60px;
}

.span-3 {
width: 180px;
}

.span-5 {
width: 300px;
}

If step is a negative number, the order of iteration is reversed:

@for $i by -1 in 1..2 {
.span-$i {
width: $i * 60px;
}
}
.span-2 {
width: 120px;
}

.span-1 {
width: 60px;
}
@for $i by -1 in 2..1 {
.span-$i {
width: $i * 60px;
}
}
.span-1 {
width: 60px;
}

.span-2 {
width: 120px;
}

To access indices, specify one more variable:

@for $icon, $i in arrow star heart {
.icon-$icon {
background-position: 0 $i * 20px;
}
}
.icon-arrow {
background-position: 0 0px;
}

.icon-star {
background-position: 0 20px;
}

.icon-heart {
background-position: 0 40px;
}

@function

You can store blocks of rules in @function, and mix them in other places with @mixin:

$clearfix = @function {
*zoom: 1;
&:before,
&:after {
content: " ";
display: table;
}
&:after {
clear: both;
}
};

ul {
@mixin $clearfix();
}
ul {
*zoom: 1;
}
ul:before,
ul:after {
content: " ";
display: table;
}
ul:after {
clear: both;
}

(Hat tip to Nicolas Gallagher, for this example uses his micro clearfix)


Or you can ask it to perform some calculations, and return the value with @return:

$width = @function {
$side-bar = 250px;
$main = 710px;

@return $side-bar + $main;
};

body {
width: $width();
}
body {
width: 960px;
}

@function can have parameters, which can also have a default value:

$button = @function $color, $bg-color, $size = large {
color: $color;
background-color: $bg-color;
@if $size is small {
font-size: 12px;
} @else if $size is large {
font-size: 14px;
}
};

#submit {
@mixin $button(#000, #fff);
}
#submit {
color: #000;
background-color: #fff;
font-size: 14px;
}

Arguments passed to a function can also be accessed dynamically using $arguments:

$social-icons = @function {
@for $icon in $arguments {
.icon-$icon {
background: url("$icon.png");
}
}
};

#social {
@mixin $social-icons(twitter, facebook);
}
#social .icon-twitter {
background: url("twitter.png");
}

#social .icon-facebook {
background: url("facebook.png");
}

Use rest parameter to capture multiple arguments:

$social-icons = @function $size, ...$icons {
@for $icon in $icons {
.icon-$icon {
background: url("$size/$icon.png");
}
}
};

#social {
@mixin $social-icons(large, twitter, facebook);
}
#social .icon-twitter {
background: url("large/twitter.png");
}

#social .icon-facebook {
background: url("large/facebook.png");
}

Unassigned parameters have value null:

$button = @function $color, $bg-color, $size {
color: $color;
background-color: $bg-color;
@if $size is null {
font-size: 12px;
} @else {
font-size: 14px;
}
};

#submit {
@mixin $button(#000, #fff);
}
#submit {
color: #000;
background-color: #fff;
font-size: 12px;
}

@extend

@extend extends other rule sets of the matching selectors:

.button {
display: inline-block;
border: 1px solid;
}

.large-button {
@extend .button;
display: block;
}
.button,
.large-button {
display: inline-block;
border: 1px solid;
}

.large-button {
display: block;
}

@extend works recursively:

.button {
display: inline-block;
border: 1px solid;
}

.large-button {
@extend .button;
display: block;
}

#submit {
@extend .large-button;
margin: 0 20px;
}
.button,
.large-button,
#submit {
display: inline-block;
border: 1px solid;
}

.large-button,
#submit {
display: block;
}

#submit {
margin: 0 20px;
}

@extend can be specified multiple times:

.button {
display: inline-block;
border: 1px solid;
}

.large-button {
@extend .button;
display: block;
}

.dangerous-button {
@extend .button;
color: #fff;
background: red;
}

#reset {
@extend .large-button;
@extend .dangerous-button;
margin: 0 20px;
}
.button,
.large-button,
.dangerous-button,
#reset,
#reset {
display: inline-block;
border: 1px solid;
}

.large-button,
#reset {
display: block;
}

.dangerous-button,
#reset {
color: #fff;
background: red;
}

#reset {
margin: 0 20px;
}

Or simply use a list of selectors:

.button {
display: inline-block;
border: 1px solid;
}

.large-button {
@extend .button;
display: block;
}

.dangerous-button {
@extend .button;
color: #fff;
background: red;
}

#reset {
@extend .large-button, .dangerous-button;
margin: 0 20px;
}
.button,
.large-button,
.dangerous-button,
#reset,
#reset {
display: inline-block;
border: 1px solid;
}

.large-button,
#reset {
display: block;
}

.dangerous-button,
#reset {
color: #fff;
background: red;
}

#reset {
margin: 0 20px;
}

Complex selectors also work as intended:

.button .icon {
font-family: icon-font;
}

.button .edit-icon {
@extend .button .icon;
content: 'i';
}
.button .icon,
.button .edit-icon {
font-family: icon-font;
}

.button .edit-icon {
content: 'i';
}

Note that selectors are matched exactly, so .icon will not match .button .icon:

.icon {
font-family: icon-font;
}

.button .icon {
font-family: button-font;
}

.button .edit-icon {
@extend .icon;
content: 'i';
}
.icon,
.button .edit-icon {
font-family: icon-font;
}

.button .icon {
font-family: button-font;
}

.button .edit-icon {
content: 'i';
}

And @extend will not extend rule sets that come after it:

.button {
display: inline-block;
}

#submit {
@extend .button;
}

.button {
display: block;
}
.button,
#submit {
display: inline-block;
}

.button {
display: block;
}

When using @extend under a @media, it will only match rule set under @media with the same media query:

.button {
display: block;
}

@media (min-width: 512px) {
.button {
display: inline-block;
}
}

@media (min-width: 1024px) {
.button {
display: inline-block;
border: 1px solid;
}
}

@media (min-width: 512px) {
#submit {
@extend .button;
}
}
.button {
display: block;
}

@media (min-width: 512px) {
.button,
#submit {
display: inline-block;
}
}

@media (min-width: 1024px) {
.button {
display: inline-block;
border: 1px solid;
}
}

@void

Rule sets inside @void are removed from the CSS output, unless they are extended by a rule set not inside a @void, but original selectors are always removed:

@void {
.button {
display: inline-block;
}

.tabs {
.tab {
@extend .button;
float: left;
}
}
}

#submit {
@extend .button;
}
#submit {
display: inline-block;
}

@import

@import imports rules from other files:

tabs.roo
.tabs {
.tab {
float: left;
}
}
@import './tabs';
.tabs .tab {
float: left;
}

.roo will be added if the file's name doesn't end with an extension.

Use relative paths (i.e., paths start with ./ or ../) to import files, future version will use paths like 'tabs' to import libraries.


Files are not imported if their paths are sepecified using url(), starting with a protocol like http://, or followed by a media query:

@import url(./tabs);

@import url("./tabs");

@import "http://example.com/tabs";

@import './tabs' screen;
@import url(./tabs);

@import url("./tabs");

@import "http://example.com/tabs";

@import './tabs' screen;

Variables will be allowed in paths in a future version.


Files are only imported once:

reset.roo
body {
margin: 0;
}
tabs.roo
@import './reset';

.tabs {
.tab {
float: left;
}
}
button.roo
@import './reset';

.button {
display: inline-block;
}
@import './reset';
@import './tabs';
@import './button';
body {
margin: 0;
}

.tabs .tab {
float: left;
}

.button {
display: inline-block;
}

@import can be nested inside other rules:

sidebar.roo
.sidebar {
float: left;
margin-left: 20px;
}
sidebar-old-ie.roo
.sidebar {
display: inline;
}
@import './sidebar';

$support-old-ie = false;

@if $support-old-ie {
@import './sidebar-old-ie';
}
.sidebar {
float: left;
margin-left: 20px;
}
framework.roo
.button {
display: inline-block;
}

.tabs .tab {
float: left;
}
@void {
@import './framework';
}

#submit {
@extend .button;
}
#submit {
display: inline-block;
}

Variable scope in the imported file is explained in the next section.

Scope

Variables have to be defined before they can be used. Once defined, they are only available within the boundary of their respective rule block, which defines their scope:

$menu = @function {
$width = 200px;
width: $width;
};

.menu {
@mixin $menu();
// $width is undefined here
}
.menu {
width: 200px;
}

Changing variables in an inner scope has no effect in outer scopes:

$width = 200px;

.mini-menu {
$width = 100px;
width: $width;
}

.menu {
width: $width;
}
.mini-menu {
width: 100px;
}

.menu {
width: 200px;
}

Variables defined in an imported file are exposed to the importing file:

vars.roo
$color = #000;
$bg-color = #fff;
@import './vars';

#main {
color: $color;
background: $bg-color;
}
#main {
color: #000;
background: #fff;
}

The imported file also has access to variables defined the importing file:

base.roo
#main {
color: $color;
background: $bg-color;
}
$color = #000;
$bg-color = #fff;

@import './base';
#main {
color: #000;
background: #fff;
}

@block

@block simply introduces a new scope, which can be used to prevent the imported file from polluting variables, and easily use default values in them:

foo-framework.roo
$width = 200px;

.foo-module {
width: $width;
}
bar-framework.roo
$width ?= 100px;

.bar-module {
width: $width;
}
@block {
@import './foo-framework';
}

@block {
@import './bar-framework';
}
.foo-module {
width: 200px;
}

.bar-module {
width: 100px;
}

@module

@module prepends a name to all class selectors inside it:

@module foo {
.button {
display: inline-block;
}

.tabs {
.tab {
float: left;

&.active {
border-bottom: none;
}
}
}
}
.foo-button {
display: inline-block;
}

.foo-tabs .foo-tab {
float: left;
}
.foo-tabs .foo-tab.foo-active {
border-bottom: none;
}

It can be used to prevent class name collisions between your project and frameworks.


By default it uses - as a separator, you can specifiy a different one using with:

@module foo with '--' {
.button {
display: inline-block;
}

.tabs {
.tab {
float: left;

&.active {
border-bottom: none;
}
}
}
}
.foo--button {
display: inline-block;
}

.foo--tabs .foo--tab {
float: left;
}
.foo--tabs .foo--tab.foo--active {
border-bottom: none;
}

@module can be nested:

@module foo {
@module bar {
.button {
display: inline-block;
}
}
}
.foo-bar-button {
display: inline-block;
}

Prefix

Roole automatically prefixes rules:

#box {
box-shadow: 0 1px 3px #000;
}
#box {
-webkit-box-shadow: 0 1px 3px #000;
-moz-box-shadow: 0 1px 3px #000;
box-shadow: 0 1px 3px #000;
}

Start position in linear-gradient() is automatically translated:

#box {
background: linear-gradient(to bottom right, #fff, #000);
}
#box {
background: -webkit-linear-gradient(top left, #fff, #000);
background: -moz-linear-gradient(top left, #fff, #000);
background: -o-linear-gradient(top left, #fff, #000);
background: linear-gradient(to bottom right, #fff, #000);
}

Currently only keywords are translated, angle values will be translated in a future version

-webkit-gradient() will be added in a future version


When properties are nested inside other rules which needs to be prefixed, Roole handles it correctly:

@keyframes become-round {
from {
border-radius: 0;
}
to {
border-radius: 50%;
}
}
@-webkit-keyframes become-round {
from {
-webkit-border-radius: 0;
border-radius: 0;
}
to {
-webkit-border-radius: 50%;
border-radius: 50%;
}
}

@-moz-keyframes become-round {
from {
-moz-border-radius: 0;
border-radius: 0;
}
to {
-moz-border-radius: 50%;
border-radius: 50%;
}
}

@-o-keyframes become-round {
from {
border-radius: 0;
}
to {
border-radius: 50%;
}
}

@keyframes become-round {
from {
border-radius: 0;
}
to {
border-radius: 50%;
}
}

Support

You can leave comments or open a new issue in the issue tracker. Before opening a new one, use the search function to make sure the issue is not already reported.

You can also follow me @curvedmark on twitter.

Change Log

0.4.1

0.4.0

0.3.1

0.3.0

0.2.1

0.2.0

0.1.2

0.1.1

0.1.0