Wha?
For years i've wanted a "minifier" for CSS (Cascading Style Sheets)
but all of them i've found are either tens of megabytes huge, with
insane amounts of dependencies, and/or are based on node.js (which i
despise with all of my cold, cold heart). Well, i'm not getting any
younger, and my desire for a CSS minifier is starting to get acute, so
i finally sat down for a few hours to Get This Done...
cssminc is a tiny CSS minifier. It's made up of only a small amount of dependency-free, portable, C89-compliant source code. It has only four small files, two C and two headers, and can be built as either a library or a standalone binary. (Building the binary also requires one additional C/header pair with app-level utility code.) The library-only bits compile down to a mere 9kb of binary code.
cssminc is intended to be pronounced "see-ess-ess mincy," "see-ess-ess min-see," "see-ess-ess mink," "see-ess-ess mince," "see-smink," or however else one prefers.
JavaScript and JSON: as of 2026-07-26 this project also hosts
jsminc, a JavaScript/JSON counterpart with the same interface
except that the configuration options necessarily differ. These docs
do not go into detail about that API because it works the same as
cssminc does. Unlike cssminc, which was 100% hand-written, jsminc was
written by an LLM. It was given cssmin and asked to produce a JS/JSON
minifier with the same interface (sans config options, which differ).
Its result required only trivial manual tweaking to (A) fix one syntax
error (nested /*...*/ comments), (B) squelch warnings from two cases
of unused arguments where an interface required a parameter but the
concrete implementation does not, and (C) remove an assert() which
was carried over verbatim from cssminc but was bogus for JS. It was
subsequently hand-refactored to consolidate the common code and
interfaces but the JS-specific pieces remain essentially entirely
LLM-written.
Features
Depending on the distribution, between two and four small, portable, strictly-C89-compatible code with no external dependencies beyond the standard C library. The binary, as opposed to the library, requires an addition two utility files.
A minuscule library API which can stream its input and output from/to any source/destination via a simple, abstract I/O interface (one function per direction of the data). The binary reads from
stdinand writes tostdout, as any well-behaved Unix-style app should.Full API documentation.
Plenty fast and relatively efficient. At any given time it only stores the previous 2 bytes of the input stream and it actively prefers constant-time logic branches (
switch/case) over linear-time ones (if/else).The core library has constant memory costs and uses no dynamic memory, but its data source/sink very probably do (opening
FILEhandles isn't cheap!).Can consume ASCII or UTF-8, including data with "weird characters" in them (so long as they're encoded as UTF-8, not latin-1, some "code page" dialect, or similar).
Can optionally try to compress everything, leaving zero (or very close to zero) extraneous space, or it can, for slightly improved readability and the avoidance of ultra-long lines, insert a space or newline after each rule block. (Some tools diagnose files with exceptionally long lines as binary, which may or may not cause usability problems.)
Misfeatures
It is not known to work 100% reliably on all valid CSS, but is also not known to not work reliably. Bug reports and patches in that regard would be much appreciated. That said: it is used to minify the CSS for my main site and several sub-sites, so it's known to work reasonably well.
It does not "lint" or validate CSS: it assumes that its inputs are already valid CSS (because they were tested for semantic validity before minification, right? Right?).
It does not explicitly support any CSS-like dialects such as SASS and LESS or whatever the cool kids are using these days. Projects using such tools already have their favourite
node.js-based infrastructure to process it. i have no intentions whatsoever of going down the rabbit hold of trying to support arbitrary non-standard CSS dialects (minor patches to that effect would be gladly accepted, so long as they're not invasive). That said: if a dialect supports the same basic format as CSS, without imposing new syntactic rules, then it "might" work as-is with this code.
License
As of 2026-07-27 it uses the SQLite Blessing "license". Prior to that it had a dual Public Domain/MIT license.
Feedback and patches are happily received: https://wanderinghorse.net/home/stephan/
Examples
The Library API:
cssminc_state cc = cssminc_state_empty; /* similar to memset() but uses some non-0 values */
int rc;
cc.base.in = minc_input_f_FILE; /* input source */
cc.base.inState = stdin; /* state for cc.in */
cc.base.out = minc_output_f_FILE; /* output destination */
cc.base.outState = stdout; /* State for cc.out */
/* cc.options.X = Y see docs */;
rc = cssminc_process(&cc)
/* 0 on success, non-0 on error, noting that "error" only
means that it choked on some of the input or one of
the I/O routines failed. */;
The in and out members can be arbitrary implementations of the I/O
abstraction functions, and the inState/outState members are opaque
state pointers for use by those functions. e.g. they could be pointers
to an app-specific memory buffer type, or the output routine might
send its contents directly to a UI widget (nothing that it might do so
a byte at a time, so the target needs to be prepared to accept, at
least briefly, partial multi-byte characters).
The CLI App
~> time ./cssminc --metrics < big.css > foo.css
./cssminc metrics:
Input bytes = 260530, output = 137715, = 47.14% reduction
Output spaces = 4188, non-spaces = 133527
real 0m0.038s
user 0m0.031s
sys 0m0.001s
(That's from a Raspberry Pi 4B system running on an external USB 2.0 hard drive, not a fast workstation.)
Output samples:
Sample (admittedly trivial) input:
$ cat foo.css
/* This comment is normally retained. */
bläüp, bläüp.x bläüp.y-z,
bläüp.zzz > x
{x:"üö" , yy : zz }/* these non-ASCII characters must survive the ccssmin process. */
/*/ <--- this is a comment, despite
weird placement of that /. */
/* MOAR comments */
foo { y:z }
/* Not retained */
bar { foo: "barre baz" }
cssminc -f foo.css -bn
The -bn tells it to add a newline after each closing brace (}):
/* This comment is normally retained. */
bläüp,bläüp.x bläüp.y-z,bläüp.zzz > x{x:"üö",yy:zz}
foo{y:z}
bar{foo:"barre baz"}
cssminc -f foo.css -bs
-bs tells it to add a space after each closing brace:
/* This comment is normally retained. */
bläüp,bläüp.x bläüp.y-z,bläüp.zzz > x{x:"üö",yy:zz} foo{y:z} bar{foo:"barre baz"}
cssminc -f foo.css -b0 -c
-b0 tells it to add nothing after closing braces and -c tells it to strip the initial comment:
bläüp,bläüp.x bläüp.y-z,bläüp.zzz > x{x:"üö",yy:zz}foo{y:z}bar{foo:"barre baz"}
Normally the initial comment of a CSS file contains a license or
attribution, and should not be stripped. cssminc only supports
keeping the first comment if it is the first non-space content in the
input. Comments appearing after non-space content are unconditionally
stripped.