LESS to Stylus Converter

Stylus is the one preprocessor with three ways to write the same rule, so this page asks which one you want before touching a line. Variables, mixins, guards, extends and escapes are rewritten. Anything Stylus has no spelling for stays as LESS and gets a mark in the output gutter.

LESS to Stylus conversion desk

LESS in
Stylus out
0 declarations carried0 selectors carried0 rewritten0 to read again0 left as LESS

One LESS file, three Stylus dialects

Most preprocessor converters on this site have one output. Going LESS to SCSS means swapping a sigil and renaming a few functions, and the shape of the file never moves. Stylus is different because Stylus itself never settled on a shape. The compiler accepts color brand, color: brand, and color: brand; inside braces, and treats all three as the same declaration. A converter with one fixed output picks a dialect for you. This one puts the choice on a dial above the panes, because the answer depends on who reads the file next, not on what the compiler accepts.

Why the dial comes first

The three settings produce the same CSS. They differ in what a reviewer sees in the pull request and in how the file reads six months later.

Variable assignments come out as name = value on every setting, since Stylus has no other spelling for them. Mixin definitions, @extend lines, and mixin calls follow the same rule and only pick up a semicolon under the braces setting.

The at-sign was doing three jobs

In LESS the @ character starts a variable, starts an at-rule, and opens an interpolation. Stylus keeps the character for at-rules only. The converter sorts each use by position:

// LESS
@brand: #0f5c5a;@wide: ~"(min-width: 62rem)";.col-@{n} { width: ~"calc(100% / @{n})"; }
@media @wide { .masthead { padding: (@gutter * 2); } }
brand = #0f5c5a
wide = unquote("(min-width: 62rem)").col-{n}
width unquote("calc(100% / " + n + ")")@media wide
.masthead
padding (gutter * 2)

A leading @name: is an assignment. A leading @media, @supports, @font-face, @keyframes, or any other name on the CSS at-rule list keeps its sigil and has its query rewritten. An at-rule name the page does not know is copied through and marked, because a LESS file with @plugin or a custom at-rule needs a human to say what it meant. Everything else with a sigil is a variable reference and loses it.

One consequence deserves a warning of its own. Stylus decides whether a bare word is a variable or a CSS keyword by checking whether the word was ever assigned. color red is the keyword red when nothing named red exists, and your variable when something does. A LESS file with @red, @auto, @none, or @large converts without error and compiles without error, and every place the keyword was meant as a keyword now reads the variable instead. The page marks each assignment whose name is also a CSS keyword. Rename those before you convert the rest of the tree.

A LESS class is a mixin. A Stylus class is a class.

LESS lets any ruleset be called as a mixin. Write .btn { padding: 8px; } and later .btn; inside another block, and the padding is copied in. Stylus separates the two. A mixin is defined without a dot and called without a dot. A class is only ever a selector.

// LESS
.pill(@bg; @fg: #fff) { background: @bg; color: @fg; }
.truncate() { overflow: hidden; white-space: nowrap; }
.tag { .pill(@brand); .truncate(); }
pill(bg, fg = #fff)background: bg
color: fg
truncate()overflow: hidden
white-space: nowrap
.tag
pill(brand)truncate()

Parameter lists come across with a comma between parameters, whichever separator the LESS used. Defaults move from @fg: #fff to fg = #fff. A rest parameter @rest... becomes rest..., and @arguments becomes arguments, both of which Stylus supports by those names.

The call that needs attention is the one with no parentheses. .btn; is a legal LESS call to a plain class. The page rewrites it as btn(), then checks whether a .btn ruleset appeared earlier in the paste and no btn() mixin did. When that is the case the line is filed under "to read again", because Stylus will report an undefined mixin. Two repairs work. Replace the call with @extend .btn if you want the selector merged, or turn the original ruleset into a real mixin and call it from the class as well.

Guards fold inside the body

LESS attaches a condition to the mixin head with when. Stylus has no head condition and instead uses if as the first line of the body. The converter moves the condition inside and reindents the body under it:

// LESS
.stripe(@c) when (lightness(@c) > 50%) and (iscolor(@c)) {border-top: 2px solid darken(@c, 30%);}
stripe(c)if lightness(c) > 50% and c is a 'rgba'
border-top 2px solid darken(c, 30%)

Three spellings change along the way. A comma between guard clauses means or in LESS and is written out as or. A single = is equality in a LESS guard and becomes ==. The type guards iscolor, isnumber, isstring, and iskeyword become the Stylus is a operator with the matching type name, and the page marks those lines because Stylus has one type for all colours and one for all numbers with units, which is coarser than what LESS was checking.

Two guard patterns have no Stylus form. A mixin defined twice with different guards, which LESS resolves by trying each in turn, has to become one Stylus function with an if and an else. The page converts each definition and marks the second one so you know to merge them. The default() guard, which matches when no other definition did, is left as LESS. So is a guard on a plain selector, such as button when (@mode = dark), since Stylus applies conditions to declarations and blocks rather than to selectors.

extend, escape, and interpolation

These three are where LESS and Stylus agree on the idea and disagree on the spelling.

A handful of functions are renamed rather than flagged. fade(@c, 12%) becomes rgba(c, 0.12), fadeout and fadein become fade-out and fade-in, greyscale becomes grayscale, and the LESS if(cond, a, b) function becomes the Stylus ternary (cond ? a : b). Colour functions with the same name on both sides, such as lighten, darken, mix, and spin, are carried by name and marked, because the two compilers round differently at the edges of the colour space and a palette is worth diffing after the first compile.

What stays as LESS

Every line the page cannot translate is copied through unchanged, given a red mark in the output gutter, and listed under "Left as LESS" with its source line number. Clicking the number moves the cursor to the line in the Stylus pane. The list is deliberate. Guessing at these would produce Stylus that compiles and does the wrong thing, which is worse than Stylus that refuses to compile until you look.

Two limits sit outside the list. The page reads one paste at a time and never follows an @import, so a mixin defined in another file looks undefined here and a class defined in another file cannot be recognised as a class. Convert the file with the definitions first. And the conversion runs entirely in this tab. The parse, the rewrite, and the receipt are JavaScript on the page. Nothing is uploaded and nothing is kept between visits.

Is Stylus where this file should go?

Two situations make this page the right one. A codebase already written in Stylus is taking on a LESS component, and one dialect in a tree is better than two. Or a build pipeline that runs Stylus, often with Nib or Kouto Swiss beside it, is inheriting a LESS theme and nobody wants a second preprocessor in the toolchain.

If neither is true, look at the destination before converting. Stylus releases have been infrequent for years, several bundler integrations treat the language as a legacy input, and the flat scope for mixins is the reason namespaces and detached rulesets have nowhere to land. A file leaving LESS with no Stylus codebase pulling it is usually better served by LESS to SCSS, where maps, control flow, and modules all have a home. A file nobody will edit again is better served by the LESS compiler, which produces plain CSS and ends the question.

When the destination is right, work down the receipt. Rename any variable the page marked as a keyword clash. Rewrite the lines under "Left as LESS". Compile the result, then compile the original with LESS to CSS and diff the two outputs. Reformat with the bare dialect only after the diff is empty, because reindenting a file that still holds LESS lines makes those lines look like they belong.

Questions about moving a LESS file onto Stylus

Dialects, keyword clashes, implicit mixins, guard merging, and the files to convert first.

Which dial setting should I pick for a file other people will review?

Braces kept, for the conversion commit itself. The diff against the LESS source then shows only the sigils, the mixin heads, and the lines the page rewrote, so a reviewer who has never read Stylus still follows every change. Switch the file to the bare dialect in a later commit once the tree compiles and the CSS diff is empty. Doing both in one commit hides the real changes inside a wall of removed braces.

My variable named @red converted with no warning in the output. Is it safe?

The assignment is marked in the receipt under "to read again", not in the output pane, because the output is valid Stylus. The problem is what Stylus does with it. Once red is assigned, every bare red in the file reads the variable, including places where you meant the CSS keyword. Rename the variable to something like brand-red before converting the rest of the tree. The page flags any variable whose name matches a common CSS keyword for this reason.

Why did .btn; become btn() with a warning?

LESS treats every class as a mixin you call by name. Stylus does not. The page saw a .btn ruleset earlier in the paste and no btn() mixin, so the rewritten call would be undefined at compile time. Use @extend .btn if you want the selector merged into the output CSS, or define a btn() mixin and have both the class and the caller use it. If the ruleset lives in another file the page never saw, the warning is a false alarm and the same choice still applies there.

Both of my guarded mixins converted. Why is the second one marked?

LESS lets you define .size() twice with different guards and tries each definition in order. Stylus resolves a name to one function, so the second definition replaces the first. The page converts both so nothing is lost, then marks the second so you merge them into one function with an if and an else. A default() guard is left as LESS outright because the else branch is the only Stylus form for it, and only you know which definition was the default.

What happened to the all keyword in my :extend?

Dropped and marked. In LESS, :extend(.tag all) also extends .tag:hover, .tag.active, and every other compound selector that contains .tag. Stylus @extend has one behaviour and no switch for the wider match. Compile and look at the CSS for the selector you extended. If the hover and state variants were part of what you needed, add an @extend for each one by name.

Why is my interpolated URL string now a concatenation?

Stylus does not interpolate inside quotes. The {name} syntax works in selectors and property names but is literal text inside a string. The page rewrites "@{host}/mark.svg" as host + "/mark.svg", which Stylus evaluates to a quoted string, and marks the line so you confirm the result is quoted where you wanted it quoted. A tilde escape such as ~"calc(100% / @{n})" comes out wrapped in unquote() so the calc() reaches the CSS without quotes.

How do I convert a whole LESS project, one file at a time?

File by file, yes, with the definitions first. The page never follows an @import, so mixins and classes from other files are unknown while it reads the current one. Convert the file holding the mixins, then the files calling them. Import lines are carried across as @import, and any LESS import option such as (reference) or (css) is dropped and marked, because Stylus reads .styl imports by extension and has no option list.

Does my LESS leave the browser?

No. The parser, the rewrite, the gutter marks, and the receipt are JavaScript running in this tab. After the page loads nothing is posted anywhere, nothing is stored between visits, and closing the tab clears both panes. A theme file from a client repository stays on your machine.

Will LESS to Stylus and back give me my original file?

On a token sheet of variables, nesting, and mixins with simple parameters, the round trip through Stylus to LESS produces a file that diffs cleanly against the original apart from formatting. On a file with detached rulesets, namespaces, or property merges it will not, because those lines were never translated and the return trip receives LESS inside Stylus. Convert only files whose "left as LESS" count reads zero if a clean round trip matters.