Your source stays put while the formatted copy builds beside it
Most beautifiers overwrite the box you pasted into. When the result comes back wrong, and with preprocessor syntax it sometimes does, the original is already gone. This page keeps the two apart. The left pane holds what you typed, the right pane rebuilds after every keystroke, and the Use formatted button moves the result across once you have read it. The parser is JavaScript running in the page, so your stylesheet never leaves the browser.
Below the panes sit two readouts. One lists anything worth a second look, such as a legacy import or a slash used for division. The other shows the selectors your nesting compiles to, written out in full rather than reduced to a depth number.
What a plain CSS formatter does to SCSS
SCSS is a superset of CSS, so a CSS beautifier handles most of a file correctly and then quietly damages the rest. Four constructs cause almost all of the trouble.
- Interpolation. A fragment written as
#{$name}contains braces. A CSS parser treats the opening brace as the start of a rule and the indentation collapses from there. The tokenizer here reads the whole fragment as one unit and never looks inside it. - Map literals. Commas inside
(sm: 36em, md: 48em)are separators inside a value, not selector separators. Line breaks only happen at commas that sit outside every bracket. - Namespaced members. After loading a module as
t, a variable readst.$space-4. The dot looks like the start of a class selector to a CSS parser. - Condition chains. A closing brace followed by
@elsebelongs to the block above it. A CSS formatter has no reason to know that and puts the two on separate lines.
$bp:(sm:36em,md:48em);@mixin above($k){@media (min-width:map-get($bp,$k)){@content}}
.card{padding:1rem;@include above(md){padding:2rem}
.card__title{font-size:1.25rem;&:hover{color:#0f766e}}}$bp: (sm: 36em,md: 48em,);@mixin above($k) {@media (min-width: map-get($bp, $k)) {@content;}}
.card {padding: 1rem;@include above(md) {padding: 2rem;}
.card__title {font-size: 1.25rem;&:hover {color: #0f766e;}}}Maps break onto their own lines, with a comma on the last pair
A breakpoint map on one line is fine at three entries and unreadable at twelve. Switching Sass maps to one pair per line splits it, and the trailing comma after the final pair is deliberate. Add a breakpoint later and the diff shows one added line instead of two changed ones, which matters on a file five people edit. Sass accepts the trailing comma in every version that supports maps.
The limit here is nesting. A map whose value is another map stays on a single line rather than opening a second level of expansion, because the indentation Sass authors use for nested maps varies by team and guessing wrong is worse than leaving it alone.
Why the else branch hugs the closing brace by default
A three branch condition is one decision, not three blocks. Keeping } @else if on one line makes the chain readable at a glance and stops the middle branch from looking like an unrelated rule that happens to sit nearby.
@mixin badge($tone) {@if $tone == danger {background: #b91c1c;} @else if $tone == warn {background: #b45309;} @else {background: #e2e8f0;}}Some house styles put every brace on its own line, so the setting flips. Both forms compile the same way in Dart Sass.
Legacy imports, and why the panel keeps mentioning them
Sass has two ways to pull in another file and they behave nothing alike. @import pastes the file in wherever it appears, so a partial named by three other partials compiles three times and every variable in it lands in one shared pool. @use loads a file once, no matter how many places reference it, and puts its members behind a namespace.
| Behaviour | @import | @use |
|---|---|---|
| Named twice in a project | Compiled twice | Compiled once |
| Variable access | Global pool, last write wins | ns.$name, traceable to a file |
| Private members | Everything is public | A leading underscore keeps it private |
| Position in the file | Anywhere | Above every style rule |
| Status in Dart Sass | Deprecated | Current |
That last row is why the note appears. Dart Sass has deprecated the import rule for Sass files and the module system is what replaces it. The panel also raises a harder error when a @use line sits below a style rule, since Sass refuses to compile that file at all.
The slash stopped meaning division
CSS uses the slash as a separator in font: 14px/1.4 and in grid shorthands. Sass used the same character for division, and the two meanings collided often enough that Dart Sass 1.33 deprecated the arithmetic reading. Division now goes through the math module.
@use "sass:math";.col {width: math.div($container, 3);}The check only fires when a variable sits on one side of the slash, which is the shape that used to mean division. A plain font: 14px/1.4 is left alone, because there it is doing its CSS job.
Nesting depth, written out as selectors
The depth counter reports the deepest single point in the file rather than an average, and the list underneath shows what those levels compile to. Seeing .page .card .body .list .item a lands differently from reading the number five. That selector needs a longer selector to override it, and it breaks the moment somebody moves a div.
The parent selector is free. Writing &:hover, &.is-active or &__title resolves into the parent instead of adding a level, so a BEM file full of ampersands stays at depth one no matter how long it gets. Only a real descendant adds to the count. When a selector list has commas, the first entry is the one shown.
Where this page stops
- SCSS syntax only. The older indented
.sassformat has no braces and no semicolons, and this parser needs both. Run those files through Sass to SCSS first. - It formats, it does not compile. Variables stay as variables and mixin calls stay as calls. For the CSS a browser receives, use SCSS to CSS or the SCSS compiler.
- No validation. A missing brace formats into tidy, deeply indented nonsense. The panel flags an unclosed block when it reaches the end of the file, though it cannot tell you which one.
- Blank lines inside blocks are dropped. Whitespace is rebuilt from scratch. Only the gap between top level rules comes back, and only with the checkbox on.
- Comment placement is approximate. A comment trailing a declaration stays on its line and a comment on its own line keeps its position. One wedged into the middle of a selector or an argument list will move.
- No sorting, prefixing or minifying. Property order is left exactly as you wrote it. For the opposite pass, send the compiled CSS to the CSS minifier.
