mrkeyoor.com_
Thu 06 Aug 07:40 UTC
npmWeb Frontendupdated 06 Aug 2026

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.

Verdict

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.

API stability4/5The plugin entry point and the preset option shape have not changed in years. Major versions mostly move individual transforms between presets and drop old Node versions, as v8 did with the declaration sorter and Node 20.
Docs4/5The site lists every optimization with before-and-after examples and documents each preset option, plus there is an online playground. Finding which option key maps to which plugin still means cross-referencing the preset page.
Maintenance4/5Pushed August 2026 with patch releases landing within days of each other, and real performance work merged through 2026. There are 79 open issues (84 issues and PRs), which is a lot, though many are individual transform edge cases.
Ecosystem5/5About 18.3M weekly downloads, the default CSS minifier in css-minimizer-webpack-plugin setups, and every transform is separately installable as a postcss-* package you can use without the preset.

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
Skip it if

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

PackageRegistryPick it when
lightningcssnpmYou want minification, vendor prefixing, and syntax lowering from one Rust binary and care more about build speed than PostCSS integration.
cssonpmYou want structural CSS optimization as a standalone library with no PostCSS or preset machinery.
clean-cssnpmA long-established Node minifier with level-based optimization settings and no peer dependency on PostCSS.
esbuildnpmYour CSS is already going through esbuild and a fast, conservative minify pass is good enough.