How a Character-Level CSS Formatter Tracks Nesting Depth
CSS was originally a language with no nesting at all. One selector, one { } block, a flat list of declarations inside — so simple counting, where every opening brace increases depth and every closing brace decreases it, always produced correct indentation. But that assumption started to wobble once preprocessors like SCSS and the modern CSS Nesting spec arrived. We opened the actual code of the CSS Beautifier to see exactly where it's safe and where it breaks down.
1. Why character counting alone was enough for plain CSS
Standard CSS syntax is a flat structure: a selector is followed by {, declarations end in semicolons, and } closes the block. Even media queries (@media) and keyframes (@keyframes) follow the exact same rule, just with one or two extra layers of braces. So a single rule — "depth+1 on an opening brace, depth-1 on a closing brace" — was enough to reproduce correct indentation for any CSS file.
2. This tool actually recognizes exactly four characters
Looking at the code, the cssBeautify() function walks the input one character at a time and only reacts to four characters: {, }, ;, and ,. Hitting { does depth++ and inserts a line break; hitting } does depth-- and inserts a line break; a line break always follows ;; and , only breaks the line when the comma option is enabled and depth is 0 (i.e., a top-level selector list). Every other character is simply copied through as-is.
3. An additional risk we found: there is no string/comment protection logic at all
The JavaScript Beautifier has an inStr() routine that protects everything inside quotes, but the CSS Beautifier's code has no such protection logic whatsoever. That means a structural character inside quotes — like content:";{}" — or a semicolon/brace inside a comment — like /* a semicolon here; */ — triggers a line break or depth change immediately, regardless of the fact that it's sitting inside a string or comment. Values like this are rare in everyday plain-CSS use, so this doesn't surface often, but it's actually a more fundamental limitation than the commonly assumed idea that "only SCSS nesting is a problem."
4. What breaks when you feed it SCSS syntax
A nested SCSS selector like &:hover{ } keeps its braces properly paired, so — surprisingly — the depth counting alone leaves its shape mostly intact. The real problem is things like a variable declaration such as $primary-color: #333;, an @if/@else conditional, or string interpolation like #{$var}. These aren't standard CSS structural characters — they're preprocessor-only tokens — and this tool has no way to distinguish them, so it mechanically inserts a line break at every semicolon just like it would for a normal declaration. The result isn't a syntax error, but SCSS's distinctive logical structure (conditional branches, variable scope) visually disappears.
| Stage | Content |
|---|---|
| Input (SCSS) | .btn{$c:#333;&:hover{color:$c;}} |
| What you'd expect from plain CSS | Nested structure accurately reflected at depth level 2 |
| Actual output | $c:#333; is treated as an ordinary declaration and gets the same line-break rule as any other property, with no notion of it being a variable |
5. Conclusion: how far can you trust it?
If you're just re-formatting plain CSS output that a server or build tool has already compiled and deployed, this approach is 100% reliable. On the other hand, if you feed it a raw .scss/.less source file, or CSS containing strings/comments with semicolons or braces inside them, don't trust the result without double-checking it. When you first minify CSS, the safest combination is running the output of the CSS Minifier back through this formatter.
FAQ
Q. Are media queries (@media) indented correctly too?
Yes. The opening brace of an @media block increases depth by 1 just like any other brace, so the selectors and properties inside it are automatically indented one level deeper.
Q. Are CSS variables (custom properties) safe?
Yes. --primary-color: #fbbf24; has the same colon-and-semicolon structure as an ordinary declaration, so it gets the same line-break and indentation rules as any other property.
Q. Is it still fine when braces are nested two levels deep, like @keyframes?
Yes. Something like @keyframes spin{ 0%{...} 100%{...} }, with two levels of nesting, still gets the same treatment — depth increases on every opening brace — so the 0% and 100% blocks each show up indented one level further in.
Q. What exactly is different from a tool like Prettier?
Prettier fully parses CSS syntax and understands the context of strings, comments, and preprocessor syntax. This tool skips that parsing step entirely and just watches for a handful of structural characters, which makes it much simpler and faster, but the result can differ in the edge cases described above.