Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Convert LESS to Stylus
// Usage : less2stylusDir('../src/css/');
var fs = require('fs');
// this less 2 stylus conversion script make a stylus easy to read syntax
// - let the braces
// - replace the @ for var as $
// - let semicolons
function less2stylus(less)
return less
// remove opening brackets
//.replace(/^(\ *)(.+)\ +\{\ *\n?\ */mg, "$1$2\n$1 ")
// remove opening brackets
//.replace(/^(\ *)([^\ \n]+)\ +\{\ *\n?\ */mg, "$1$2\n$1 ")
// remove opening brackets again (some random cases I'm too lazy to think through)
//.replace(/\ *\{\ *\n*/g, "\n")
// remove closing brackets
//.replace(/\ *\}\ *\n*/g, "\n")
// remove semicolons
//.replace(/\;\ *?$/gm, "")
// replace @variable: with $variable =
.replace(/@(\w+):(\ *)\ /g, function(_, $1, $2) {
return "$" + $1 + $2 + " = ";
// replace all other variable call, careful with native @{keyword}
.replace(/\@(\w+)/g, function(_, $1) {
if ($1 === "import" || $1 == "media") {
return _;
} else {
return "$" + $1;
// replace @{variable} with {$variable}
.replace(/@\{(\w+)\}/g, function(_, $1) {
return '{$' + $1 +'}';
// replace mixins from .border-radius(4px) to border-radius(4px)
.replace(/\.([\w-]+) ?\(/g, "$1(")
// switch this two lines if you want to disable @extend behavior
//.replace(/(\.[a-zA-Z][\w-]+;)/g, "@extend $1") // replace mixins without args by @extend
.replace(/\.([a-zA-Z][\w-]+);/g, "$1();") // replace mixins without args
.replace(/(\ *)(.+)>\ *([\w-]+)\(/g, "$1$2>\n$1 $3(")
// ms filter fix
.replace(/filter: ([^'"\n;]+)/g, 'filter: unquote("$1")')
// url data
.replace(/: ?url\(([^'"\)]+)\)/g, ': url(unquote("$1"))')
// rename (useless)
.replace(/\.less/g, ".styl")
// tinies optimizations
// make all commas have 1 space after them
.replace(/,\ */g, ", ")
// replace 0.x by .x
.replace(/(:\ )0\.([0-9])+/g, ".$2 ")
// remove trailing whitespace
.replace(/\ *$/g, "");
function less2stylusDir(dir)
var names = fs.readdirSync(dir);
for (var _j = 0, _len = names.length; _j < _len; _j++) {
var name = names[_j];
if (name.match(/\.less$/))
var stylus = less2stylus(fs.readFileSync(dir + "/" + name, "utf-8"));
fs.writeFileSync(dir + "/" + (name.replace(/\.less$/, ".styl")), stylus);
// if you have a folder tree, add each folder, or use glob()...

Post conversion

  • Some @import orders have been switched to don't broke Stylus parsing
  • Some .mixin have been rename to .mixin() for a better conversion


First of all, I've created a base script which made most of the conversion

node less2stylus.js

This convert every .less to .styl equivalent Please note this less to stylus conversion script make a simple but easier to read syntax Stylus do not require this but this script keep the braces, the semicolons & a variable identifier ($ instead of @ a la Sass) Since less just have simple mixin, Stylus have @extend Sass feature, so I've optimized some mixin call without args by replacing them with @extend. The stylus output will probably be lighter than the less one

After conversion: stylus parse try

Fixes of "parse error": I've change just some tiny stuffs. Mainly;

  • url(data...) to url(unquote('data...'))
  • same for microsoft filters

Manual Output comparison

stylus --out _output index.styl

lessc index.less > _output/index.less.css

Since I've used @extend with less2stylus(), I can do a simple comparison. So I modified a little bit the less code to be able to try comparision without @extend (it was hard since less has same syntax for class & mixins).

Comparison results

Things that are not a problem

  • By default, stylus does not include /* */ comments so there some differences but really not important
  • The opacity() mixin is completly broken but it's not really a problem. It's just less than 10 lines. & Stylus nib extension have that
  • Some url('') have been changed to url(""), it's not a problem.
  • Some color value have been optimized (e.g. : black to #000) or lowercased

Main issue

Becareful, there is a sort of major with if a less mixin, which are also class name. So for some mixin called without args, you have to add the class name after the mixin declaration.

mixin() { // previously .mixin used as a mixin

.mixin {

Only tiny issue

Same Color functions return differents results e.g. :

  • less lighten(#E0E3E4, 5%) => #eeeff0
  • stylus lighten(#E0E3E4, 5%) => #e2e4e5
  • sass lighten(#E0E3E4, 5%) => #eeeff0

The difference is no easily visible, but anyway, I've bumped this issue Compare the difference vs

Copy link

banacorn commented Jan 5, 2013

hello, I get this when converting bootstrap.

blitz:cedar-bark blitz$ stylus --out banana stylus/bootstrap.styl

              throw err;
ParseError: stylus/mixins.styl:140
   136|     #font > #family >
   137|       serif();
   138|     #font >
   139|       shorthand($size, $weight, $lineHeight);
 > 140|   }
   141|   sans-serif($size = $baseFontSize, $weight = normal, $lineHeight = $baseLineHeight) {
   142|     #font > #family >
   143|       sans-serif();

unexpected "}"

    at Parser.error (/usr/local/lib/node_modules/stylus/lib/parser.js:166:11)
    at Parser.stmt (/usr/local/lib/node_modules/stylus/lib/parser.js:578:32)
    at Parser.statement (/usr/local/lib/node_modules/stylus/lib/parser.js:460:21)
    at Parser.block (/usr/local/lib/node_modules/stylus/lib/parser.js:608:21)
    at Parser.selector (/usr/local/lib/node_modules/stylus/lib/parser.js:1118:24)
    at Parser.stmt (/usr/local/lib/node_modules/stylus/lib/parser.js:567:27)
    at Parser.statement (/usr/local/lib/node_modules/stylus/lib/parser.js:460:21)
    at Parser.block (/usr/local/lib/node_modules/stylus/lib/parser.js:608:21)
    at Parser.selector (/usr/local/lib/node_modules/stylus/lib/parser.js:1118:24)
    at Parser.stmt (/usr/local/lib/node_modules/stylus/lib/parser.js:567:27)

Copy link

vjpr commented May 10, 2013

This is broken for variables with dashes in them. @cvans fork fixes it:

Copy link

jdeebee commented May 11, 2014

Thank you man! It's great!

Copy link

Archakov06 commented Nov 10, 2016

Script replaces @font-face on $font-face

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment