CSS Minification Best Practices: What Survives the Build Step
Minification has one rule that outranks every other rule: the transformation must not change what the browser sees inside a string. Everything else — whitespace, shortening colours, dropping the last semicolon — is cosmetic and reversible. Strings are not.
The failure you should assume is happening
.a::before { content: "x/*y*/z" }
.b::after { content: "}" }
.c::before { content: "12:30" }
.d { background: url("a;b.png") }
A textual minifier that strips /* … */ before it knows where strings start turns line 1 into content:"xz". A textual beautifier that inserts newlines at every } cuts line 2's block in half. Line 3 is the one that fools people: it survives in most tools, which builds false confidence in the tool right up until line 1 costs you an afternoon.
This site's own tool has both failures, and they are reproduced against the shipped code — with the actual outputs — on About. The minifier survives the quoted }; the tidier does not. The minifier does not survive the quoted comment. Reading your own tool's limits in your own words is cheaper than discovering them in a production bundle.
The general rule: if a tool cannot tell a string from a comment, it cannot be trusted with either.
The order of transforms matters more than the transforms
Every minifier is a sequence of steps, and the order is where the bugs live:
- Tokenise (or don't — everything below is unsafe if you skipped this).
- Strip comments.
- Remove whitespace that is not semantically required.
- Collapse values (
#ffffff→#fff,0.5px→.5px, drop trailing;). - Merge/shrink rules.
Steps 2 and 3 are the dangerous pair. Doing them as regex over raw text is the entire bug class in the section above. If you are writing your own minifier — for a build step, a static site, an in-browser tool like this one — that is the order to implement, and step 1 is not optional if the input is anything users paste in.
What is safe to assert in CI
Fuzz the boundary instead of trusting the tool. Cheap version, no dependencies:
# Run the real tool over inputs chosen to break textual transforms,
# then diff. Any output where a string literal changed length = a real bug.
for probe in \
'a::before{content:"x/*y*/z"}' \
'a::after{content:"}"}' \
'a{background:url("a;b.png")}' \
'a{content:"12:30"}'
do
out=$(my-minifier <<<"$probe")
[ "$out" = "$probe" ] || { echo "MANGLED: $probe -> $out"; exit 1; }
done
Those four lines are the whole test suite, and they take under a second. Most minifiers that fail them fail all of them, and most teams find out during a release.
Two more assertions worth having when you depend on someone else's minifier:
- Byte-budget check on a known input. Pin one CSS file, record its minified hash, fail if the hash moves after a toolchain upgrade. Minifier upgrades change output; that is fine, silent is not.
- Round-trip readability check. Unminify the output and compare declarations. You will not get byte-identical input back, and you should not expect to — but a declaration that vanished is a different thing from a reformatted one.
Rules that survive review
- Never minify a file you have not looked at once. A hand-maintained stylesheet with a
content: "}"in it is not rare, and minification is where the bug becomes invisible. - Keep your source readable; minify only in the build artifact. If you are reading minified CSS to debug, your pipeline is missing a sourcemap, not a cleverer minifier.
- Let the minifier drop the last semicolon, not your formatter add the first two. Rules like "always one declaration per line" belong in a linter with a fix mode; encoding them as blind text substitution is how a formatter starts losing data.
- Pin the minifier's version.
+56%download growth on a package last published 2023-11-30 (clean-css, registry pull 2026-10-12) is evidence that people inherit these tools rather than review them. Your minifier is a dependency with a maintainer; check they still exist. - Comments that must survive need a marker. If a banner/license comment has to ship, the tool needs an explicit rule to keep it (
/*! … */by convention in most PostCSS-era tools). "It kept it last time" is not a rule.
Where this site fits
csstidy.net is textual on purpose: no install, no upload, nothing leaving the browser, works offline on a plane. Those are real benefits for the 20 KB stylesheet you are inspecting, and they are bought with the exact limitations above. It is the wrong tool for a production bundle, and the alternatives page is the honest ranking of things that parse.