cssnano
cssnano is a CSS minifier that runs as a PostCSS plugin. Instead of one monolithic optimizer it is a bundle of about thirty small PostCSS plugins, each doing one job: collapse whitespace, shorten hex colours, drop comments, merge margin longhands into a shorthand, deduplicate rules, run SVG data URIs through SVGO, and so on. Which plugins run is decided by a preset. The default preset only applies transforms that cannot change how the page renders; the advanced preset adds ones that can, like rewriting z-index values, and you opt into those knowingly. Because it sits inside PostCSS it shares the AST with Autoprefixer and Tailwind, so a typical build parses your CSS once and hands the same tree down the chain.
If PostCSS is already in your pipeline, cssnano is the safe default and its per-plugin switches will get you out of trouble when one transform misbehaves. If you are choosing a minifier from scratch in 2026, benchmark lightningcss first, because the output is close and the build is much faster.
Use it if
- You already run PostCSS for Tailwind, Autoprefixer, or nesting, and want minification in the same pass instead of re-parsing the stylesheet with a separate tool
- You want the smallest realistic output and are willing to pay build time for it: merging longhands, deduplicating rules, and reducing selectors typically beats a whitespace-and-colour pass
- You need per-transform control, for example keeping comments that carry licence text while dropping everything else, or turning off the SVGO pass because it mangles an icon you rely on
- Your output has to respect Browserslist; several of the transforms read your browserslist config and back off when an older target cannot handle the shorter form
- Build speed is the constraint. cssnano runs roughly thirty AST passes over your CSS; lightningcss and esbuild minify in a fraction of the time, and on a large design system the difference is seconds per build, not milliseconds
- You are not otherwise using PostCSS. Pulling in postcss plus cssnano plus the whole default preset just to strip whitespace is a lot of dependency surface for that job
- You are on Node 20 or older. v8 declares engines of Node 22.11, 24.11, or 26 and newer, so upgrading cssnano can force a runtime upgrade you did not plan
- You expect the advanced preset to be safe. It rebases z-index, merges and renames identifiers, and discards unused at-rules, all of which break the moment a third-party widget or a runtime-injected style depends on the original values
- Your bundler already minifies CSS for you. Vite, Next.js, and Parcel ship a minifier by default; adding cssnano on top means the stylesheet gets minified twice and you pay for both
Setup reality
npm install cssnano postcss, then add cssnano to the plugins list in postcss.config.js. postcss is a peer dependency pinned to ^8.5.25, so an old postcss in the tree gives you a peer warning from npm and a hard failure from pnpm. Two configuration surprises follow. First, cssnano only reads an external config file when you pass no preset option at all; give it a preset inline and lilconfig never runs, so your .cssnanorc is silently ignored. Second, the search list is package.json, .cssnanorc, .cssnanorc.json, .cssnanorc.js, and cssnano.config.js, which means an .mjs or .cjs config file is not found. The advanced preset is a separate install, cssnano-preset-advanced. Most setups also want minification only in production, since running it on every dev rebuild is wasted time.
Patterns
Add cssnano to postcss.config.jspostcss-config-basic
// postcss.config.js
module.exports = {
plugins: [
require("autoprefixer"),
require("cssnano")({ preset: "default" }),
],
};Order matters: cssnano should be last so it minifies whatever the earlier plugins produced. Passing preset inline stops cssnano from reading any external config file.
Only minify in production buildsproduction-only
// postcss.config.js
const plugins = [require("autoprefixer")];
if (process.env.NODE_ENV === "production") {
plugins.push(require("cssnano")({ preset: "default" }));
}
module.exports = { plugins };Roughly thirty AST passes on every hot reload is time you never get back, and minified CSS makes devtools source mapping worse. Gate it on the build mode.
Configure through a config file insteadconfig-file
// cssnano.config.js
module.exports = {
preset: [
"default",
{
discardComments: { removeAll: true },
normalizeWhitespace: true,
},
],
};Only found when you call cssnano with no preset option. Searched names are package.json, .cssnanorc, .cssnanorc.json, .cssnanorc.js, and cssnano.config.js; an .mjs or .cjs file will not be picked up.
Pass options to individual transformstune-preset-options
require("cssnano")({
preset: [
"default",
{
colormin: false,
convertValues: { length: false },
minifySelectors: { sort: false },
},
],
});Each key is a plugin in the preset. false switches the plugin off entirely; an object is forwarded to that plugin. { exclude: true } also disables one, which is the form the preset types document.
Strip comments but keep licence bannerskeep-license-comments
require("cssnano")({
preset: ["default", { discardComments: { remove: (comment) => !comment.startsWith("!") } }],
});By default cssnano keeps /*! ... */ comments and removes the rest, so you often need no config at all. Setting removeAll: true deletes licence text too, which some dependencies require you to ship.
Use the aggressive presetadvanced-preset
// npm install cssnano-preset-advanced
require("cssnano")({
preset: [
"advanced",
{
zindex: false,
discardUnused: { fontFace: false },
},
],
});The advanced preset is a separate package and its transforms assume they can see all of your CSS. Rebasing z-index breaks any third-party widget that hardcodes a stacking value, so most people disable that one first.
Stop it from rewriting inline SVG data URIsdisable-svgo
require("cssnano")({
preset: ["default", { svgo: false }],
});The svgo pass optimizes SVGs embedded in url() values. It occasionally changes rendering of gradients or masks; if an icon suddenly looks wrong after adding minification, turn this off first.
Run it from Node without a build toolprogrammatic-api
const postcss = require("postcss");
const cssnano = require("cssnano");
const result = await postcss([cssnano({ preset: "default" })])
.process(css, { from: "src/app.css", to: "dist/app.css" });
await fs.writeFile("dist/app.css", result.css);Always pass from and to, or PostCSS warns and source maps come out wrong. process() returns a lazy result, so you must await it or read .css to make the work happen.
Wire it into webpackwebpack-minimizer
// webpack.config.js
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
"...",
new CssMinimizerPlugin({
minimizerOptions: { preset: ["default", { discardComments: { removeAll: true } }] },
}),
],
},
};css-minimizer-webpack-plugin uses cssnano by default, so do not also add cssnano to postcss-loader or the CSS is minified twice. The "..." entry keeps webpack's JS minifier in place.
Override the browser targets it optimizes forbrowserslist-targets
require("cssnano")({
preset: ["default", { overrideBrowserslist: ["> 0.5%", "last 2 versions", "not dead"] }],
});Several transforms consult Browserslist before shortening a value. Without an override it reads your project browserslist config, and with no config at all it falls back to defaults that may be wider than you want.
Keep source maps through minificationsource-maps
const result = await postcss([cssnano({ preset: "default" })]).process(css, {
from: "src/app.css",
to: "dist/app.css",
map: { inline: false, prev: previousMap },
});
await fs.writeFile("dist/app.css.map", result.map.toString());Source map handling belongs to PostCSS, not cssnano. Pass the upstream map as prev or the chain breaks and every rule points at the wrong line.
Skip presets and pick transforms yourselfcustom-plugin-list
require("cssnano")({
preset: {
plugins: [
[require("postcss-discard-comments"), { removeAll: true }],
[require("postcss-normalize-whitespace"), {}],
[require("postcss-colormin"), {}],
],
},
});Each transform is its own npm package, so you can build a minimal minifier with three of them. Order is yours to get right, and you lose the preset's tested sequencing.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lightningcss | npm | You want minification, vendor prefixing, and syntax lowering from one Rust binary and care more about build speed than PostCSS integration. |
| csso | npm | You want structural CSS optimization as a standalone library with no PostCSS or preset machinery. |
| clean-css | npm | A long-established Node minifier with level-based optimization settings and no peer dependency on PostCSS. |
| esbuild | npm | Your CSS is already going through esbuild and a fast, conservative minify pass is good enough. |