Why a stylesheet moves back to braces
The indented syntax arrived first, in 2006, and still has people who prefer it. Most Sass work now happens in SCSS, and files drift in this direction for practical reasons rather than taste. Five of them come up in almost every migration.
- Pasted CSS has to work. A rule copied out of devtools, a vendor prefix block, a keyframes snippet from a blog post. All of it drops into a .scss file with no edits, because every valid CSS file is already valid SCSS. The same paste into a .sass file is a syntax error until you strip the punctuation by hand.
- The ecosystem publishes SCSS. Bootstrap, Bulma and Foundation ship .scss sources. Overriding their variables means writing in their syntax, and the documentation you copy from uses braces.
- Tooling assumes braces. Prettier formats SCSS and skips the indented syntax. Stylelint coverage for .sass files is thinner. Editor extensions and syntax highlighters get more attention pointed at .scss.
- New people read it on day one. A developer who knows CSS reads SCSS immediately. The indented syntax needs a short explanation first, and a whitespace bug is harder to spot in a pull request than a missing brace.
- Block boundaries survive the diff. Indented files are shorter, which is a real benefit. Reindent one rule and every line under it changes in the diff with nothing to anchor the reviewer. Braces keep the boundary visible.
None of this makes the indented syntax a mistake. A team writing every line in house, with nobody pasting CSS from outside, keeps shorter files and cleaner declarations by staying where they are. Convert when the friction shows up in review, not because braces are the default.
The five rewrites this converter performs
Sass reads both syntaxes with one compiler, so this is a reformatting job rather than a translation. Five rewrites cover a normal stylesheet. Everything else copies across as written.
Indentation becomes braces
Each indent level opens a block and the dedent closes it. The parser looks ahead one line to work out whether a line opens a block or ends a statement, so a selector with deeper lines under it gets braces while a declaration does not.
SASS in.card padding: 1rem &:hover border-color: $brandSCSS out.card {padding: 1rem;&:hover {border-color: $brand;}}Line ends become semicolons
A declaration in the indented syntax finishes where the line finishes. SCSS wants the semicolon spelled out. At-rules with no block attached take one too, so imports and module loads are covered by the same pass.
SASS in@use "sass:math" $gutter: 24px .grid gap: math.div($gutter, 2)SCSS out@use "sass:math";$gutter: 24px;.grid {gap: math.div($gutter, 2);}Shorthand directives get written out
The indented syntax has a shorthand pair with no SCSS equivalent.
=namedefines a mixin and+namecalls one. Both get expanded to the long form, which is the only spelling the brace syntax accepts. The rail counts how many were rewritten.SASS in=card-shadow($blur: 24px)box-shadow: 0 8px #{$blur} rgba(0,0,0,.08).panel +card-shadow(32px)SCSS out@mixin card-shadow($blur: 24px) {box-shadow: 0 8px #{$blur} rgba(0,0,0,.08);} .panel {@include card-shadow(32px);}Backslash continuations fold into one line
A long value in a .sass file runs onto a second line with a trailing backslash, the one continuation marker the syntax accepts. SCSS ends the declaration at the semicolon instead, so the fragments join and the backslash goes away.
SASS in.panel transition: background 120ms ease, \ box-shadow 120ms easeSCSS out.panel {transition: background 120ms ease, box-shadow 120ms ease;}Joined values stay on one line. Rewrap them by hand where your line limit complains, since SCSS allows a value to sit across several lines with no marker at all.
@else rejoins the brace of its @if
This is the rewrite a line-by-line script gets wrong. In the indented syntax an
@elsesits at the same depth as its@if. Under braces it has to follow the closing brace on the same line, or Sass reports an else with no matching if and the build stops.SASS in@if $theme == dark .page background: #111 @else .page background: #fffSCSS out@if $theme == dark {.page {background: #111;}} @else {.page {background: #fff;}}Chains of
@else iffollow the same rule, and the whole chain gets walked. Switching the brace control above to next-line style leaves the closing brace alone and puts the else underneath it, which is the shape Allman-style formatters expect.
Judgment calls the parser makes for you
Some lines have more than one reasonable reading. Here is what happens to each one and the reasoning behind it.
- Braces inside a value stay inside the value
#{$size}and a string likecontent: "{ a }"both carry braces with no block behind them. Quote state and interpolation depth are tracked apart from nesting depth, so neither one opens a level. A converter built on find and replace either eats the expression or pushes every line below it one level too deep.- A nested property keeps its trailing colon
font:with nothing after it opens a namespace rather than a declaration. The output writesfont: {and the children keep their own colons. Dropping the parent colon turns the line into a selector named font, and the compiler stops at the first child.- A loud comment gets its closing marker back
- A
comment in the indented syntax ends when a line returns to the parent indent, so plenty of .sass files never write the*/at all. The output adds it where the indentation ran out. Re-read any comment carrying ASCII alignment, because the continuation lines get re-indented to the block they sit in. - A trailing comment keeps its position
- The semicolon goes in front of the comment rather than at the end of the line.
background: transparent // notebecomesbackground: transparent; // note. Appending the semicolon after the comment text would bury it and leave the declaration unterminated. - A childless selector becomes an empty block
- A selector with nothing indented under it has no declarations to wrap, so the output writes
.name {}and the rail flags it. Sass drops empty rules at compile time, which means these are either a leftover or a sign the indentation slipped a level. - SCSS input is left untouched
- When most lines already end in a brace or a semicolon, the input is SCSS and a conversion pass would nest every block a second time. The text passes through unchanged with a warning. Send it the other way through the SCSS to SASS converter.
Moving a project across, one partial at a time
A whole-repo conversion in a single commit is close to unreviewable, since every line in the diff changed. Splitting the work keeps the history readable and the build green throughout. Sass reads .sass and .scss files in one project and imports cross the boundary in both directions, so a half-converted repo compiles without complaint.
- Compile and keep the current CSS. Save the built stylesheet before touching anything. This file is what you compare against later, and the comparison is the only check proving nothing moved.
- Start with leaf partials. Files nothing imports carry the least risk. Variable and mixin libraries come after, since a slip there shows up in every rule at once.
- Convert, then rename. Paste one file above, copy the SCSS out, then change the extension. Import paths stay as they are, because
@useand@importreference a file without its extension. - Read the rail before committing. The notes name empty rules, uneven indentation and expanded shorthands. Each of those marks a spot where the source was ambiguous and a human should look.
- Compile again and diff the CSS. Byte-identical output is the target. A difference means the nesting shifted, and the diff points straight at the rule.
- Keep reformatting out of behavior commits. One commit per batch of converted files with no other edits inside it. Reviewers skim a pure reformat and read a real change properly.
- Switch the tooling on last. Add Prettier and the stylelint SCSS rules once every extension has changed, so the formatter does not rewrite half-migrated files underneath you.
Watch the files you have not converted yet. Mixed tabs and spaces are a hard error in a .sass file and a non-issue in .scss. A source file Sass has been rejecting for weeks starts compiling the moment it converts, and the rule it produces might not be the one anyone expected.
Where this converter stops
- It reformats, it does not compile. The output is SCSS source, not a stylesheet a browser reads. Send the result to the SCSS to CSS converter, or take the indented source straight to the SASS to CSS compiler when CSS is all you wanted.
- Nothing gets validated. An undefined variable, a mixin called with the wrong argument count and a misspelled property all convert cleanly and fail at build time. The parser reads indentation structure, not meaning.
- Formatting is preserved, not normalized. Spacing inside a value, quote style and color notation come through exactly as written. Run the result through the SCSS beautifier to apply house style afterwards.
- Mixed tabs and spaces produce a guess. Sass rejects such a file outright. This parser measures a tab as four spaces so you get output at all, and warns you in the rail. Reindent the source with one character and convert again for a result worth trusting.
- Large files run inside the tab. One pass over a few thousand lines finishes instantly. A framework bundle of tens of thousands will stutter on a phone, and partial by partial is how the output should be reviewed anyway.
