clean-css
clean-css is a CSS minifier written in plain JavaScript. You construct it with an options hash, call minify() with a CSS string or a list of file paths, and get back an object holding the optimized styles plus stats, warnings, and errors. Optimizations are grouped into three levels: level 0 does nothing beyond reading and inlining, level 1 (the default) rewrites single properties such as shortening colors and dropping units from zeros, and level 2 works across rules, merging duplicates, folding longhands into shorthands, and optionally restructuring rulesets so they take fewer bytes. It also inlines local @import rules, rebases relative URLs, and produces source maps. It is the minifier behind grunt-contrib-cssmin, gulp-clean-css, and a large slice of older webpack setups, which is where most of those weekly downloads come from.
clean-css still minifies ordinary CSS correctly and it is deeply embedded in legacy build tooling, which is the only good reason left to pick it. For anything new, use lightningcss or whatever your bundler already includes; a CSS parser that has not shipped since 2023 is a bad bet as the language keeps growing.
Use it if
- You already run a Grunt, Gulp, or older webpack build whose CSS plugin wraps clean-css, and swapping minifiers would mean rewriting the asset pipeline for a few kilobytes of difference
- You want cross-rule optimizations that most minifiers skip: level 2 merges non-adjacent rules with the same selector, removes properties that are redefined later, and can restructure rules so declarations move to shorter homes
- You need @import inlining and URL rebasing done by the minifier itself, because your CSS is assembled from files on disk rather than by a bundler that already resolved them
- You are minifying CSS at runtime inside Node with no build step, and want a single dependency-light package with a synchronous call rather than a bundler or a native binary
- You care about the project still being developed: the README states outright that clean-css is in maintenance mode, the last release 5.3.3 landed in November 2023, and the repository was last pushed in October 2024. Bugfixes are occasional and by the author's own description best-effort
- You write modern CSS: the parser was built in the IE-compatibility era and defaults to an ie10+ compatibility mode. Nesting, container queries, cascade layers, and other newer syntax are exactly the places where an unmaintained CSS parser quietly drops declarations it does not recognize
- Your bundler already ships a minifier: esbuild, Vite, and Parcel all minify CSS natively, and lightningcss is a Rust parser that is far faster and understands current syntax. Adding clean-css to those pipelines buys nothing
- You want plugin-shaped control over transformations: cssnano runs on PostCSS, so it shares an AST with autoprefixer and your other PostCSS plugins, while clean-css has its own private AST and a much smaller plugin surface added in v5
- You need the command line: the CLI was split out in 4.0 and lives in a separate clean-css-cli package, so npm install clean-css gives you a library and no binary
Setup reality
npm install clean-css pulls one dependency (source-map 0.6) and no native code, and the API is CommonJS with no ESM export map, so ESM consumers get the default-import interop shuffle. The parts that catch people out are behavioral. minify() is synchronous by default and returns the result object directly, but it becomes asynchronous the moment you pass a callback or set returnPromise: true, and remote @import rules are only inlined when you pass a callback because fetching is async; without one they are left untouched. If you pass a string it is treated as CSS source, and if you pass an array of strings they are treated as file paths, which is a surprising overload the first time you hit it. Since 5.0, rebase defaults to false, so relative url() paths are left alone unless you set rebaseTo. Errors do not throw: invalid CSS is dropped and reported in output.warnings and output.errors, which nobody reads until a rule goes missing in production.
Patterns
Minify a CSS stringminify-a-string
const CleanCSS = require('clean-css');
const output = new CleanCSS().minify('a { color: blue; }\ndiv { margin: 5px }');
console.log(output.styles); // a{color:#00f}div{margin:5px}
console.log(output.stats.efficiency); // fraction of bytes savedWithout a callback, minify() is synchronous and returns the result object directly. Nothing throws on bad CSS, so check output.errors and output.warnings yourself.
Turn on cross-rule optimizationsoptimization-level-2
const CleanCSS = require('clean-css');
const output = new CleanCSS({ level: 2 }).minify(source);Level 2 merges duplicate and non-adjacent rules and folds longhands into shorthands. Level 1 still runs underneath unless you explicitly disable it. restructureRules stays off even at level 2; enable it separately if you want rules moved around.
Mix level 1 and level 2 optionsfine-grained-levels
const CleanCSS = require('clean-css');
const output = new CleanCSS({
level: {
1: { all: true, normalizeUrls: false },
2: { all: false, removeDuplicateRules: true, restructureRules: true }
}
}).minify(source);The all flag sets every option at that level, so list it first and then re-enable the ones you want. This is the escape hatch when one specific optimization breaks your stylesheet.
Minify files without reading them firstminify-files-from-paths
const CleanCSS = require('clean-css');
// array of strings means "these are paths"
const output = new CleanCSS().minify(['src/base.css', 'src/theme.css']);
console.log(output.styles); // both files concatenated and optimizedA bare string is treated as CSS source; the same string inside an array is treated as a file path. Passing paths is also what lets clean-css resolve local @import rules and rebase URLs correctly.
Optimize many files without concatenating thembatch-multiple-files
const CleanCSS = require('clean-css');
const output = new CleanCSS({ batch: true }).minify(['src/a.css', 'src/b.css']);
console.log(output['src/a.css'].styles);
console.log(output['src/b.css'].stats.minifiedSize);Added in 5.0. With batch: true the return value is keyed by input path instead of being one merged result, and each entry carries its own styles, stats, errors, and warnings.
Get a promise or use a callbackasync-and-promises
const CleanCSS = require('clean-css');
new CleanCSS({ returnPromise: true })
.minify(source)
.then((output) => console.log(output.styles))
.catch((err) => console.error(err));
// or the callback form
new CleanCSS().minify(source, (error, output) => {
if (error) return console.error(error);
console.log(output.styles);
});returnPromise must be asked for explicitly; the default is synchronous. Mixing the two (returnPromise plus a callback) is not supported, pick one.
Inline remote @import rulesinline-remote-imports
const CleanCSS = require('clean-css');
const source = '@import url(https://example.com/base.css);';
new CleanCSS({ inline: ['local', 'remote'] }).minify(source, (error, output) => {
console.log(output.styles);
console.log(output.inlinedStylesheets);
});Remote inlining only happens in the callback or promise form, because the fetch is async. Call it synchronously and the @import is silently left in the output. Default is inline: 'local'.
Keep a block of CSS untouchedpreserve-css-fragments
/* clean-css ignore:start */
.critical-hack {
color: transparent;
}
/* clean-css ignore:end */
/*! this bang comment survives minification */The ignore markers are a source-level feature (4.2+), not an option. Comments starting with /*! are kept by default via the level 1 specialComments option; set it to 0 to strip licence headers too.
Produce a source mapgenerate-source-maps
const CleanCSS = require('clean-css');
new CleanCSS({ sourceMap: true, rebaseTo: 'dist' })
.minify(['src/app.css'], (error, output) => {
// output.sourceMap is a SourceMapGenerator instance
require('fs').writeFileSync('dist/app.min.css.map', output.sourceMap.toString());
});sourceMap must be a boolean; an incoming map is passed as the second argument to minify() instead. Set rebaseTo to the output directory or the paths inside the map point at the wrong place.
Emit readable CSS instead of one lineformat-readable-output
const CleanCSS = require('clean-css');
const pretty = new CleanCSS({ format: 'beautify' }).minify(source);
const breaks = new CleanCSS({ format: 'keep-breaks' }).minify(source);
const custom = new CleanCSS({
format: { breaks: { afterRuleEnds: 2 }, indentBy: 2, indentWith: 'tab' }
}).minify(source);Since 5.0 break options accept numbers, so afterRuleEnds: 2 means two newlines. Useful when you want optimization without an unreadable single-line diff in a committed file.
Loosen or tighten browser compatibilitycompatibility-mode
const CleanCSS = require('clean-css');
new CleanCSS({ compatibility: 'ie9' }).minify(source);
// or one flag at a time
new CleanCSS({ compatibility: 'ie9,-properties.merging' }).minify(source);
new CleanCSS({
compatibility: { customUnits: { rpx: true } }
}).minify(source);The default is still ie10+, which is why some modern-looking output choices seem conservative. Non-standard units such as rpx are dropped as invalid unless you register them under customUnits.
Write a plugin to rewrite propertiescustom-plugin
const CleanCSS = require('clean-css');
const dropRepeatedRepeat = {
level1: {
property: function (_rule, property) {
if (property.name === 'background-repeat' && property.value.length === 2
&& property.value[0][1] === property.value[1][1]) {
property.value.pop();
property.dirty = true;
}
}
}
};
new CleanCSS({ plugins: [dropRepeatedRepeat] }).minify(source);Plugins replaced the old transform callback in 5.0. You mutate the internal property representation and must set dirty = true or the change is not serialized. The AST shape is barely documented; read lib/optimizer/level-1/property-optimizers for working examples.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lightningcss | npm | You want a maintained minifier that parses current CSS syntax and runs an order of magnitude faster because it is Rust |
| cssnano | npm | Your build is already PostCSS-based and you want the minifier to share one AST pass with autoprefixer and friends |
| esbuild | npm | You are bundling anyway and would rather have CSS minification come free with the bundler than as a separate step |
| csso | npm | You want structural CSS optimization on a csstree parser with a smaller, more inspectable codebase |