SASS to Stylus Converter

Indented SASS and Stylus already share their shape. What separates them is a handful of characters: the dollar sign, the equals and plus on mixins, the at-sign on control flow. This page rewrites each of those, lets you decide whether $ stays on variable names, and lists every line worth a second look before you save the .styl file.

SASS to Stylus rewrite board

Dollar sign on variable names
SASS in
Stylus out

Rewrite ledger

    Read again before saving

      Two indented syntaxes, one sigil apart

      Of the four preprocessor syntaxes, indented SASS and Stylus are the two with no braces and no semicolons. Nesting is carried by whitespace in both. So a conversion between them never has to rebuild structure the way SASS to SCSS does. What remains is a series of small decisions about single characters: the $ on a variable, the = and + on a mixin, the @ on an if, the % on a placeholder, the #{} around an interpolation. Most SASS to Stylus converters make those decisions silently. This one puts the one contested decision on a switch, lists every rewrite in a ledger, and marks the lines where the meaning shifted even though the syntax came out clean.

      Why the dollar sign gets its own switch

      Stylus treats $ as an ordinary identifier character. $brand = #5b2a86 compiles, and color: $brand resolves. The Stylus documentation and most published .styl files drop the sign, so brand = #5b2a86 is the idiom you will see in a codebase already written in Stylus. Both are correct output. They differ in one failure mode.

      SASS
      $white: #fdfcfa
      $radius: 6px !default
      
      .card
        background: $white
        border-radius: $radius
        color: white
      Stylus, dollar dropped
      white = #fdfcfa
      radius ?= 6px
      
      .card
        background: white
        border-radius: radius
        color: white   // now #fdfcfa, not the keyword

      Stylus resolves a bare identifier as a variable before falling back to the CSS keyword of the same name. Once white is assigned, every later white in the file reads the variable. The same trap exists for black, transparent, none, auto, bold, center, and the named colours. The converter checks each dropped name against a list of those keywords and adds an amber mark to the line, so you decide whether to rename the variable or flip the switch to keep the sign. Keep the sign when the SASS file names its colour tokens after colours, or when you want $ to stay greppable across a mixed codebase. Drop the sign when the file is joining a Stylus project whose style guide already reads bare.

      !default becomes the conditional assignment ?=, which assigns only when the name is still undefined. !global has no Stylus form. The flag is removed and the line is marked, because a Stylus assignment inside a mixin or rule stays scoped to the block around the line.

      The plus sign changes sides

      In SASS the sigils on mixins are fixed: =card defines, +card includes. In Stylus a mixin is a function whose body contains selectors or declarations, so both the definition and the call are written as card(). The + survives in one place only. A Stylus mixin whose body contains {block}, the counterpart of @content, has to be called with a leading plus: +respond(48rem) followed by an indented block. The converter pre-scans every mixin definition for @content, records which names are block mixins, and writes the plus on those calls. A call followed by an indented body is treated as a block call even when the definition lives in another file.

      SASS
      =card($pad: 16px)
        padding: $pad
      
      =respond($bp)
        @media (min-width: $bp)
          @content
      
      .panel
        +card(20px)
        +respond(48rem)
          width: 100%
      Stylus
      card(pad = 16px)
        padding: pad
      
      respond(bp)
        @media (min-width: bp)
          {block}
      
      .panel
        card(20px)
        +respond(48rem)
          width: 100%

      Default arguments switch from $pad: 16px to pad = 16px. Keyword arguments at the call site, +card($pad: 20px), come out as card(pad: 20px), which Stylus accepts as named arguments. @include name using ($x) is left as SASS with a red mark, since Stylus passes nothing into a block.

      Control flow loses the at-sign and keeps the words

      Stylus control flow reads like a scripting language. @if becomes if, @else if becomes else if, @while becomes while. Comparison operators, and, or and not are the same words in both. Loops change more:

      Division needs parentheses in Stylus

      Sass has spent years steering people from $a / 2 to math.div($a, 2). Stylus has the opposite rule. A bare slash in a property value is a literal slash, because font: 14px/1.5 Inter has to survive. Division happens inside parentheses or inside a function call's argument list. The converter turns math.div(15px, 16px) into (15px / 16px), wraps a bare $columns / 8 in parentheses with an amber mark, and leaves percentage($i / 4) alone because the call parentheses already make the slash divide. A 14px/1.5 with no variable on either side is never touched.

      Construct by construct

      SASS Stylus Note
      $brand: #5b2a86brand = #5b2a86The switch decides whether $ stays.
      $radius: 6px !defaultradius ?= 6pxConditional assignment.
      =card($pad: 16px)card(pad = 16px)Defaults use =.
      +card(20px)card(20px)Plain call, no sigil.
      +respond(48rem) with a body+respond(48rem)Block call keeps the plus.
      @content{block}
      %muted, @extend %muted$muted, @extend $mutedPlaceholders keep $ on both settings.
      .gap-#{$name}.gap-{name}
      #{&}{selector()}Marked.
      $sizes: (sm: 4px, md: 8px)sizes = {sm: 4px, md: 8px}One-line maps only.
      map-get($sizes, md)sizes[md]Nested keys chain brackets.
      map-has-key($sizes, md)(md in sizes)
      nth($steps, 2)steps[1]Stylus indexes from zero. Marked when the index is a variable.
      if($dark, #000, #fff)(dark ? #000 : #fff)
      transparentize($c, .2)fade-out(c, 20%)Alpha scaled to a percentage. Marked.
      adjust-hue($c, 30)spin(c, 30)
      @debug $mp(m)@warn and @error become warn() and error().
      @use "sass:math"commented outThe functions are Stylus built-ins.
      @use "tokens" as t@import 'tokens't. is stripped from every member below. Marked.

      Nested properties get flattened

      Indented SASS allows a property to open a block: font: on one line, family: and size: indented below, compiling to font-family and font-size. Stylus has no nested property form. The converter runs an early pass joining the parent and child names with a hyphen and moving each child up to the parent's indent. A parent line carrying its own value, font: 15px with children beneath, keeps the value as its own declaration and then emits the children.

      What comes back marked

      Two colours appear in the output gutter and in the list under the panes. Clicking any entry in the list moves the cursor to the line.

      Where this converter stops

      Should this file become Stylus at all?

      Two reasons hold up. The file is joining a project already written in Stylus, or the build already runs the Stylus compiler and adding Dart Sass to the toolchain is the thing nobody wants. Outside those two cases, moving from indented SASS to Stylus trades an active compiler for a slow one. Dart Sass ships releases most months. Stylus releases arrive a few times a year, and the module system, math.div, @forward and @use ... with have no counterpart, which is why they come back red above.

      If the reason for leaving .sass is thin editor support for the indented syntax, the nearer move is SASS to SCSS. Same compiler, same features, every formatter and linter works, and the change is punctuation only.

      When Stylus is the right call, convert one partial at a time, compile both sides, and read the amber list before the red one. The red lines fail loudly. The amber lines are the ones a diff will not catch.

      SASS to Stylus questions

      Does Stylus allow a dollar sign in variable names?

      Yes. Stylus treats $ as a normal identifier character, so $brand = #5b2a86 and color: $brand both compile. Most Stylus code drops the sign, which is why the switch defaults to dropping it. Keeping the sign avoids a variable named white or none shadowing the CSS keyword of the same name.

      Why did one @include come out with a plus sign and the others without?

      In Stylus a plain mixin is called like a function, card(20px). A mixin whose body contains {block}, the Stylus form of @content, has to be called with a leading plus and an indented body: +respond(48rem). The converter scans every mixin definition for @content and writes the plus only on calls to those mixins, or on any call followed by an indented block.

      What happens to a Sass map?

      A one-line map, $sizes: (sm: 4px, md: 8px), becomes a Stylus hash, sizes = {sm: 4px, md: 8px}. map-get becomes a bracket lookup, sizes[md], and map-has-key becomes (md in sizes). map-keys, map-values and map-merge become keys(), values() and merge(). If a bare key does not resolve after compiling, quote it.

      Why is nth($list, 1) now list[0]?

      Sass lists count from one and Stylus lists count from zero. A literal index is shifted down by one. When the index is a variable, the converter writes list[n - 1] and adds an amber mark so you read the expression once before saving.

      Does the converter accept SCSS with braces?

      No. The parser reads indentation to find nesting, and braces are passed through as selector text. Use the SCSS to Stylus converter for .scss files. Both indented and braced Stylus output are valid, so the choice of input page follows the source file, not the output you want.

      Why do declarations keep their colons by default?

      Stylus accepts color: brand and color brand as the same declaration. Keeping the colon produces the smallest diff against the SASS source and reads unambiguously when a value starts with a word. Tick the Strip colons box to write the bare form used in most Stylus style guides.