CSS formatter
Lay out CSS over lines and indents without rewriting a single value — the token stream is proved identical apart from whitespace.
- SECURE
- NO UPLOADS
- NO SIGNUP
- BROWSER BASED
- FREE
- FOREVER.
Support us with a link or a share
- UTF-8
- —
- Characters
- —
- Input
- —
Only whitespace moves — the result is verified token by token before it appears here.
What this CSS formatter proves, and what it refuses to claim
Most tools that tidy a stylesheet are quietly rewriting it. They shorten colors, swap quotation marks, drop a unit from a zero, add the semicolon you left off the last declaration, or delete a comment that looked decorative. Each of those is a defensible edit and none of them is formatting. The difference matters when the file you are tidying is one you did not write, or one that a browser bug is currently working around.
This tool moves whitespace and nothing else, and it checks the claim twice before it hands you anything. First the laid-out stylesheet is tokenized again and the token stream is compared with the token stream of your input; a single token that differs in type or in spelling means no result is returned at all. Then both texts are stripped of every whitespace character and the remaining characters are compared in order, without a parser in the loop. The two checks are deliberately different measurements: a tokenizer that stopped telling two tokens apart would produce matching signatures for texts that are not the same, and the character comparison cannot fail in that direction.
Both checks passed on all 191 stylesheets in this site's own source tree — roughly 1.46 million characters of production CSS, including cascade layers, nesting, escaped identifiers, custom properties, and at-rules the parser has never heard of. Running the output through the formatter a second time changed nothing in any of them, which is the property that separates a formatter from a rewriter.
Whitespace is rebuilt, not preserved
The honest version of the promise is narrower than “you get your file back.” Your non-whitespace characters come back byte for byte in the same order. The whitespace does not: it is thrown away and written again from scratch. Indentation you had is replaced by the indentation you chose. Runs of blank lines between rules collapse to a single blank line, and every top-level rule gets one blank line before the next. A missing final newline is added. Windows line endings become Unix ones, which is the one character-level change the tool makes outside your tokens, and the input panel says so when it happens.
There are exactly three places where a space appears that you never typed: after the colon
of a declaration, after the colon of a condition such as @media (min-width: 768px),
and after a comma. A colon inside :not(), selector() or an
@scope selector is a pseudo-class and never gets one. Everywhere else the original spacing between two
tokens is carried through unchanged, including the space in calc(100% - 2rem)
that separates an operator from a signed number, and including the odd spacing in
color: red ! important, which stays odd because normalizing it would be an
edit.
That has one consequence worth knowing about, and it is the one place where “only
whitespace moved” is still a difference a browser will show you. A custom property keeps
its value as a token sequence, spaces included. If you wrote
--font:Helvetica,Arial, the formatted file says
--font: Helvetica, Arial, and the browser keeps the extra space in two
visible places: the string a script reads back from the computed value, and the rule's own
serialized text in cssRules. Every other measurement we could take of the two
stylesheets came back identical — same rules, same computed color, display, width and
padding — so the cascade is unaffected and font-family: var(--font) resolves
the same. Code doing an exact string comparison against a custom property is the exception,
and it is worth knowing before you reformat a file that has one.
Where the layout is deliberately unclever
There is one rule for line breaks: a block opens a level, a semicolon ends a line, and
each selector in a comma-separated list gets its own line. The formatter has no opinion
about anything else, and that is a choice rather than an omission. Selector combinators
are never normalized, because in [a~="b"] the tilde and the equals sign are
two separate tokens and deciding where a space belongs between them is a judgment about
selectors. A missing semicolon on the last declaration is not supplied, because adding one
changes the token stream and the verification would rightly fail.
The unclever rule has a cost, and it is easiest to see in
grid-template-areas. If you stacked the row strings on separate lines to draw
the grid in the source, the formatter joins them onto one line, because to it they are
simply four string tokens in a value. Nothing is lost and nothing is changed, but the
picture you drew is gone. If a stylesheet leans on that trick, format it before you draw
the grid, not after.
Comments are kept, and kept where you put them. A comment that stood alone on its own line
gets its own line; one that sat in front of a value on the same line stays in front of that
value. A comment in front of a rule gets its own line even when minified source glued it to
the selector, because /* Theme */:root { is hard to read and the line break
between them means nothing to CSS. That is the opposite of what a minifier does, and it is why the two directions are
two tools.
What it refuses, and the one valid stylesheet it gets wrong
Sass is refused before any work happens, and the message names the construct it found.
This is not squeamishness about a preprocessor: a proof that the token stream is unchanged
only proves the meaning is unchanged in a language where whitespace carries no meaning
line by line. CSS is such a language. SCSS, with // comments that run to the
end of a line, is not — moving a line break there would let a comment swallow the code
behind it — so a refusal is a stronger answer than a proof after the fact.
The screen that detects those constructs is a text scan, and one shape of valid CSS trips
it. A custom property whose value begins a line with an unquoted protocol-relative URL,
as in a declaration that continues onto the next line with
//cdn.example.com/x.png, is reported as a Sass line comment and refused. The
same URL inside url() or inside quotation marks formats normally. It is a
false refusal rather than a wrong result, which is the direction to fail in, but it is a
limitation and not a feature.
Unbalanced braces are not refused, because a browser reads them too: a { that
is never closed swallows every rule after it. The layout follows what was written, so the
rules after it come out indented one level deeper, and the receipt says how many braces
were left open or had nothing to close.
Two more refusals exist. A string or a url() that is never closed is rejected
up front, because the end of the file closes it under the specification and a line break
moved into it would not. And a stylesheet longer than 16,777,216 characters is refused
because the bundled parser packs a token's offset into 24 bits; past that, valid CSS gets
read as broken syntax. Nothing is refused for being merely large, and nothing is uploaded
to be checked.
Formatting and minifying are one stream read in two directions
The opposite direction is a separate tool on its own URL: the CSS minifier takes the same token stream and removes every space the grammar does not need. It parses rather than re-lays-out, so its guarantees are stated differently, and it is the right tool for a build artifact rather than for a file you are about to read. Round-tripping a stylesheet through both is not lossless in the formatting sense, because minification is allowed to drop comments the layout keeps.
Sibling formats have their own grammars and their own hazards, so they have their own tools rather than a mode switch: the HTML formatter has to respect elements where whitespace is content, the JSON formatter works on a data structure rather than a token stream, the XML formatter has namespaces and mixed content to protect, and the SVG optimizer rewrites geometry on purpose. If you want to see exactly which lines a reformat touched, run the before and after through the text diff. Everything here happens in this tab; the stylesheet you paste or open is never sent anywhere.
Questions
Does this CSS formatter change any values?
No. Only whitespace moves. A short hex color stays short, single quotes stay single, and a unit is never normalized. The formatted output is re-tokenized and compared with the input token stream, so a change to anything other than whitespace means no result is returned at all.
Can it format SCSS or Sass?
No, and it says so rather than trying. A proof that the token stream is unchanged only proves the meaning is unchanged in a language where whitespace carries no line-wise meaning. CSS is such a language; SCSS, with its line comments and interpolation, is not. Sass constructs are detected and refused before any work is done.
Why are combinators left alone?
Because a formatter that normalizes them would be editing. In `[a~="b"]` the tilde and the equals sign are two separate tokens, and deciding where a space belongs between them is a judgment about selectors, not about layout.
Is a missing final semicolon added?
No. Adding one would be an edit, not a formatting change, and the token stream would no longer be identical. The formatter lays out what you wrote.