CSSTidy CSS Formatter

Beautify Is Not Format: Three Implementations, Same 23 Inputs

CSSTidy — CSS Formatter & Minifier Guides · Updated 2026-10-11 · All guides

"Beautify" and "format" get used as synonyms and are not the same operation. A beautifier re-inserts whitespace into text. A formatter parses and re-prints from a tree. The difference is invisible on well-behaved CSS and total on everything else, so I ran the same 23 inputs through three implementations and one 95 KB real file.

Two of the three are here for contrast, including the tool on this page. I would rather publish the cases where this site loses.

Setup, reproducible

cd /home/walshie/code/sites/dev-utilities/scripts/fuzz
node -e 'const {loadApp}=require("./load");
  const api=loadApp("csstidy",["beautifyCss","minifyCss"]).api;
  console.log(JSON.stringify(api.runHandler(api.beautifyCss,"a { color: red; }")))'
# → "a {\n  color:red;\n  \n}"

runHandler drives the real Format button handler from apps/csstidy/public/index.html, not a reimplementation. The comparators are [email protected] (jb.css(src)) and [email protected] (parser: "css"), installed in a scratch dir; both are the current latest per https://registry.npmjs.org/<pkg> pulled 2026-10-11.

The shipped beautifier is five regexes:

text.replace(/\s*([\{\};:])\s*/g, "$1")
    .replace(/;/g, ";\n  ")
    .replace(/\{/g, " {\n  ")
    .replace(/\}/g, "\n}\n\n")
    .replace(/  \}/g, "}");

No input in the battery is "unusual". A content string containing } is the single most normal thing in an icon font's stylesheet.

Where they differ, measured

Output is JSON.stringify-quoted so \n and trailing spaces are visible. ✓ = semantically identical to input, ✗ = output differs in a way that changes rendering or validity.

Inputcsstidy beautifyCssjs-beautify 2.0.3Prettier 3.9.10
div::before{content:"}"}✗ "div::before {\n content:\"\n}\n\n\"\n}"✓ "div::before {\n content: \"}\"\n}"✓ "div::before {\n content: \"}\";\n}\n"
div::before{content:"a;b"}✗ "div::before {\n content:\"a;\n b\"\n}"✓ content: "a;b"✓ content: "a;b";
a{background:url(data:image/png;base64,AAA=)}✗ split at ;: "a {\n background:url(data:image/png;\n base64,AAA=)\n}"✓ "a {\n background: url(data:image/png;base64,AAA=)\n}"✓ same, semicolon added
@media (min-width: 600px) { .a { color: red; } .b { … } }✗ nested rules at parent indent, stray " \n" before each }✓ .a and .b indented 4, @media closes cleanly✓ .a/.b at 4 spaces
a{}b{color:red}"a {\n \n}\n\nb {…}""a {}\n\nb {\n color: red\n}""a {\n}\nb {…}"
a{color:red;}✗ "a {\n color:red;\n \n}" — leaves a line of two spaces✓ "a {\n color: red;\n}"✓
a{color:red;;}"a {\n color:red;\n ;\n \n}""a {\n color: red;\n ;\n}"✓ "a {\n color: red;\n}\n"
a , b , c { color : red }"a , b , c {\n color:red\n}" — selector spacing untouchedone selector per lineone selector per line
a{margin: 10px 20px}margin:10px 20px (triple space survives)margin: 10px 20pxmargin: 10px 20px;
a{color:var(--x, red)}color:var(--x, red)color: var(--x, red)color: var(--x, red);
@media screen and (min-width:768px){.a{color:red}}✗ same flat-indent problem, }\n\n\n}✓ correct nesting✓ correct nesting
/*! keep me */\na{color:red}✓ comment survives✓ comment survives✓ comment survives
a{content:"x/*y*/z"}✓ survives (unlike this tool's minifier — see gotchas)✓✓
a{content:"12:30"}✓✓✓
a{color:#ff0000;background:#ffffff}✓ colours untouched✓ untouched✓ untouched
a{margin:0px;padding:0em;border-width:0}✓ units untouched✓ untouched✓ untouched
a{*zoom:1;color:red}✓✓✓
a{_height:20px}✓✓✓
a{-webkit-box-shadow:…;box-shadow:…}✓✓✓
a{width:calc(100% - 20px);margin:calc(1px + 2px)}✓ spacing kept✓ kept✓ kept
a{background:url( my image.png )}✓ kept verbatimurl(my image.png ) — one inner space eatenurl(my image.png);
a { color: red; } (well-behaved)"a {\n color:red\n}""a {\n color: red\n}""a {\n color: red;\n}\n"
a{color:red} (well-behaved)"a {\n color:red\n}"same shapesame shape

Six inputs where this page's output is wrong; js-beautify and Prettier: zero. All six are the same root cause — the beautifier has no tokeniser, so it cannot tell a structural ; or } from one inside a string. That is the whole difference between beautify and format.

The 95 KB file, where the story is less clean than the table

[email protected] (from https://unpkg.com/[email protected]/animate.css), 95,374 bytes in. One run each, same Node process:

ToolOutput bytesvs inputf(f(x)) === f(x)
csstidy beautifyCss91,450−4.1%true
js-beautify 2.0.3104,751+9.8%true
Prettier 3.9.1095,423+0.05%true

Three things this measures that the table above cannot.

The shipped beautifier is the only one that makes the file smaller, and that is not a virtue — it is because it strips a semicolon before } and collapses value whitespace, i.e. it is doing half a minify while pretending to be a pretty-printer. A "readable" output that is smaller than the input should make you suspicious of what it deleted.

All three are idempotent. I checked because the naive expectation is that a regex-based tool would drift on re-application (format(format(x)) adding blank lines forever). It does not: beautify(beautify(media)) returned a byte-identical string. So the "I pasted it back in and formatted again" loop is safe. Strings are not.

Prettier is essentially byte-neutral (+0.05%) because animate.css is already Prettier-formatted upstream. js-beautify's +9.8% is its 4-space default plus blank lines between rules. The shipped tool's −4.1% is the anomaly, and I would not call that output "formatted".

What the shipped beautifier cannot do, by construction

None of these are bugs to file; they are absent features implied by "beautify", and the absence is why a formatter exists:

  • Indentation by depth. Indent comes from the string " " inserted after {, never from a depth counter. A rule inside @media sits at its parent's indent — visible in every @media row above. Nested @media inside @media is unrepresentable.
  • Semicolon normalisation. It cannot add the missing semicolon Prettier adds to color: red before }, nor remove the stray ; from ;;. It can only relocate the ones already there.
  • Any decision. Sorting, grouping, blank-line policy, line-width wrapping, quote style, trailing commas — all formatter concerns, all requiring a tree. a , b , c keeps its spaces because nothing decided anything about it.

Choosing between them, without opinions

  • Reading a snippet you will not ship: the shipped beautifier, because it is one page load and one clipboard paste, and content strings are rare in tutorials. Check them if the snippet has icons.
  • Anything with icons, counters, or data: URIs: a formatter. Two of the six failures above are content: strings and one is an inline data: URI — those three cover most real stylesheets.
  • A repository: Prettier or Biome in CI with the config committed, so the question never comes up. Formatting debates are a symptom of an unenforced config, not of bad tooling.
  • js-beautify specifically: it handled every case Prettier did, and it is the smaller download if you are shipping a formatter into a browser bundle. Measured, not estimated: js-beautify's browser file is 54,387 bytes (https://unpkg.com/[email protected]/js/lib/beautify-css.js) against Prettier's browser build at 81,957 bytes (https://unpkg.com/[email protected]/standalone.mjs) — about two-thirds, not the order-of-magnitude gap the install tarballs suggest (196 KB against 2.8 MB). Ship the file you measured.

The one-line version

A beautifier asks "where do braces go". A formatter asks "what is this file". Only the second question can be answered without corrupting a content: "}", and I measured all three rather than asserting it.

Developer Sponsor / Partner
Copied to clipboard!