TL;DR
PostCSS is the CSS transformation framework — plugin-based architecture with 350+ plugins (Autoprefixer, Tailwind CSS, cssnano), the most widely used CSS processing tool. Lightning CSS is the Rust-based CSS parser/transformer — handles autoprefixing, nesting, minification, and bundling in a single tool, 100x faster than PostCSS. cssnano is the PostCSS-based CSS minifier — modular optimization presets, removes unused code, merges rules, the most popular CSS minifier. In 2026: PostCSS for plugin ecosystem, Lightning CSS for speed and all-in-one processing, cssnano for CSS-only minification.
Key Takeaways
- PostCSS: ~100M weekly downloads — plugin framework, powers Autoprefixer + Tailwind + cssnano
- Lightning CSS: ~15M weekly downloads — Rust-native, all-in-one (prefix + nest + minify + bundle)
- cssnano: ~25M weekly downloads — CSS minifier, PostCSS plugin, modular presets
- PostCSS is a framework; Lightning CSS and cssnano are tools
- Lightning CSS replaces PostCSS + Autoprefixer + cssnano in one tool
- Vite uses Lightning CSS as an optional CSS processor (replacing PostCSS)
PostCSS
PostCSS — CSS transformation framework:
Basic usage
import postcss from "postcss"
import autoprefixer from "autoprefixer"
import cssnano from "cssnano"
const css = `
.container {
display: flex;
user-select: none;
&:hover {
color: oklch(0.7 0.15 200);
}
}
`
const result = await postcss([
autoprefixer(),
cssnano({ preset: "default" }),
]).process(css, { from: "input.css", to: "output.css" })
console.log(result.css)
// → .container{display:flex;-webkit-user-select:none;user-select:none}
// → .container:hover{color:oklch(.7 .15 200)}
Popular plugins
// postcss.config.js
module.exports = {
plugins: [
// Autoprefixer — add vendor prefixes:
require("autoprefixer"),
// Nesting — CSS nesting support:
require("postcss-nesting"),
// Custom properties fallback:
require("postcss-custom-properties"),
// Import — inline @import:
require("postcss-import"),
// Preset Env — use future CSS today:
require("postcss-preset-env")({
stage: 2,
features: {
"nesting-rules": true,
"custom-media-queries": true,
},
}),
// Minification:
require("cssnano")({ preset: "default" }),
],
}
With Tailwind CSS
// postcss.config.js
module.exports = {
plugins: {
"tailwindcss": {},
"autoprefixer": {},
},
}
// Tailwind CSS is built as a PostCSS plugin:
// 1. PostCSS parses your CSS
// 2. Tailwind plugin generates utility classes
// 3. Autoprefixer adds vendor prefixes
// 4. cssnano minifies (in production)
Build tool integration
// Vite — PostCSS config auto-detected:
// Just create postcss.config.js and Vite uses it
// Webpack:
module.exports = {
module: {
rules: [{
test: /\.css$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
"autoprefixer",
"cssnano",
],
},
},
},
],
}],
},
}
// Next.js — postcss.config.js auto-detected
Writing a PostCSS plugin
import { Plugin } from "postcss"
const myPlugin: Plugin = {
postcssPlugin: "postcss-add-dark-mode",
Rule(rule) {
// Add dark mode variant for every color declaration:
rule.walkDecls("color", (decl) => {
const darkRule = rule.clone()
darkRule.selectors = darkRule.selectors.map(
(sel) => `.dark ${sel}`
)
darkRule.removeAll()
darkRule.append(decl.clone({ value: "white" }))
rule.parent?.insertAfter(rule, darkRule)
})
},
}
Lightning CSS
Lightning CSS — Rust-based CSS processor:
Basic usage
import { transform, bundle } from "lightningcss"
// Transform a single file:
const { code, map } = transform({
filename: "style.css",
code: Buffer.from(`
.container {
display: flex;
user-select: none;
&:hover {
color: oklch(0.7 0.15 200);
}
}
`),
minify: true,
sourceMap: true,
targets: {
chrome: 95 << 16, // Chrome 95+
firefox: 90 << 16, // Firefox 90+
safari: 15 << 16, // Safari 15+
},
})
console.log(code.toString())
// → .container{display:flex;-webkit-user-select:none;user-select:none}.container:hover{color:oklch(.7 .15 200)}
All-in-one processing
import { transform } from "lightningcss"
// Lightning CSS handles everything PostCSS + plugins does:
const { code } = transform({
filename: "style.css",
code: Buffer.from(css),
minify: true,
// Browser targets (replaces Autoprefixer):
targets: {
chrome: 90 << 16,
firefox: 88 << 16,
safari: 14 << 16,
},
// CSS Modules:
cssModules: true,
// Drafts (enable experimental features):
drafts: {
customMedia: true,
},
// Nesting (built-in, replaces postcss-nesting):
// Automatically lowered based on targets
// Custom properties (built-in):
// Automatically lowered based on targets
// Color functions (built-in):
// oklch, lab, lch converted based on targets
})
CSS bundling
import { bundle } from "lightningcss"
// Bundle CSS with @import resolution:
const { code, map } = bundle({
filename: "src/main.css",
minify: true,
targets: {
chrome: 95 << 16,
},
})
// main.css:
// @import "./reset.css";
// @import "./components/button.css";
// @import "./utilities.css";
//
// → All imports inlined and bundled into one file
CSS Modules
import { transform } from "lightningcss"
const { code, exports } = transform({
filename: "Button.module.css",
code: Buffer.from(`
.button {
background: blue;
color: white;
}
.primary {
composes: button;
background: green;
}
`),
cssModules: true,
minify: true,
})
console.log(exports)
// → {
// button: { name: "Button_button_abc123", composes: [] },
// primary: { name: "Button_primary_def456", composes: [{ type: "local", name: "Button_button_abc123" }] },
// }
With Vite
// vite.config.ts
export default {
css: {
transformer: "lightningcss", // Use Lightning CSS instead of PostCSS
lightningcss: {
targets: {
chrome: 95 << 16,
firefox: 90 << 16,
safari: 15 << 16,
},
drafts: {
customMedia: true,
},
},
},
build: {
cssMinify: "lightningcss", // Use Lightning CSS for minification too
},
}
// No postcss.config.js needed!
// Lightning CSS handles prefixing, nesting, minification
cssnano
cssnano — CSS minifier:
Basic usage
import postcss from "postcss"
import cssnano from "cssnano"
const css = `
.container {
margin: 10px 20px 10px 20px;
color: #ff0000;
font-weight: bold;
background-color: rgba(0, 0, 0, 0.5);
}
.unused { }
/* This is a comment */
`
const result = await postcss([
cssnano({ preset: "default" }),
]).process(css)
console.log(result.css)
// → .container{margin:10px 20px;color:red;font-weight:700;background-color:rgba(0,0,0,.5)}
// Comments removed, shorthand properties, color shortening, etc.
Presets
// Default preset — safe optimizations:
cssnano({ preset: "default" })
// Advanced preset — more aggressive (may break some CSS):
cssnano({ preset: "advanced" })
// Lite preset — minimal, fastest:
cssnano({ preset: "lite" })
// Custom configuration:
cssnano({
preset: ["default", {
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
minifyFontValues: true,
minifyGradients: true,
reduceTransforms: true,
svgo: true,
calc: true,
// Disable specific optimizations:
zindex: false, // Don't rebase z-index
discardUnused: false, // Keep unused @keyframes
}],
})
Optimizations explained
cssnano optimizations:
Color minification:
#ff0000 → red
#ffffff → #fff
rgba(0,0,0,0.5) → rgba(0,0,0,.5)
rgb(255,0,0) → red
Shorthand merging:
margin: 10px 20px 10px 20px → margin: 10px 20px
padding: 5px 5px 5px 5px → padding: 5px
Value normalization:
font-weight: bold → font-weight: 700
font-weight: normal → font-weight: 400
Duplicate removal:
Removes duplicate declarations and rules
Calc simplification:
calc(2 * 50px) → 100px
calc(100% - 0px) → 100%
SVG minification:
Optimizes inline SVG in background-image
Comment removal:
Strips all comments (configurable)
With build tools
// Vite — cssnano used automatically in production:
// vite.config.ts
export default {
// cssnano is the default CSS minifier in Vite
// Or use postcss.config.js:
// plugins: [cssnano({ preset: "default" })]
}
// Webpack:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin")
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({
minimizerOptions: {
preset: ["default", { discardComments: { removeAll: true } }],
},
}),
],
},
}
// Next.js — cssnano used automatically in production builds
Feature Comparison
| Feature | PostCSS | Lightning CSS | cssnano |
|---|---|---|---|
| Purpose | CSS framework | All-in-one processor | CSS minifier |
| Language | JavaScript | Rust (WASM/native) | JavaScript |
| Speed | Slow | Very fast (100x) | Medium |
| Autoprefixing | Via plugin | ✅ built-in | ❌ |
| CSS nesting | Via plugin | ✅ built-in | ❌ |
| Minification | Via cssnano plugin | ✅ built-in | ✅ |
| CSS Modules | Via plugin | ✅ built-in | ❌ |
| Bundling (@import) | Via plugin | ✅ built-in | ❌ |
| Plugin ecosystem | 350+ plugins | ❌ | Modular presets |
| Custom transforms | ✅ (plugin API) | ✅ (visitor API) | ❌ |
| Used by | Tailwind, Next.js | Vite (optional) | Webpack, Vite |
| Weekly downloads | ~100M | ~15M | ~25M |
When to Use Each
Use PostCSS if:
- Need Tailwind CSS or other PostCSS plugins
- Want the largest plugin ecosystem
- Need custom CSS transforms (write your own plugin)
- Speed is not a bottleneck in your build
Use Lightning CSS if:
- Want maximum build speed
- Need autoprefixing + nesting + minification in one tool
- Using Vite and want to replace PostCSS
- Don't need PostCSS-specific plugins (like Tailwind)
Use cssnano if:
- Need CSS minification only (not processing/transforms)
- Already using PostCSS and want to add minification
- Want modular, configurable optimization presets
- Using Webpack with css-minimizer-webpack-plugin
PostCSS Plugin Architecture and Custom Transforms
PostCSS's plugin API is mature and well-documented, making it the right choice for teams with custom CSS transformation needs. A PostCSS plugin is a function that receives the parsed CSS AST and can walk, modify, insert, or remove nodes. The plugin API covers rules (selectors), declarations (property: value pairs), at-rules (@media, @keyframes), and comments. Plugins are composable and ordered — each plugin processes the AST before passing it to the next. This architecture enables powerful transformation pipelines: a plugin that extracts design tokens from CSS custom properties, another that generates utility classes, and a final pass that minifies. The 350+ community plugins cover almost every CSS processing need, from px-to-rem conversion to RTL flipping to inline SVG optimization. Writing your own plugin is approachable — the PostCSS API documentation and existing plugins provide ample patterns to follow.
Lightning CSS Performance in the Vite Ecosystem
Lightning CSS's integration with Vite represents a significant performance improvement for large CSS codebases. PostCSS-based CSS processing in Vite uses Vite's PostCSS transform pipeline, which processes CSS files sequentially in Node.js. Lightning CSS replaces this with a Rust-native parallel processor that handles multiple CSS files simultaneously. For a project with 500+ CSS files (large enterprise apps, monorepos with many component libraries), the CSS build step can drop from 10-30 seconds to under a second with Lightning CSS. The css.transformer: "lightningcss" Vite config option is a one-line change that requires no changes to existing CSS code since Lightning CSS understands the same CSS syntax PostCSS does. The caveat: Tailwind CSS v3 requires PostCSS and does not work with Lightning CSS as the primary transformer — though Tailwind v4 moves away from PostCSS dependency.
CSS Minification Depth and Bundle Size Impact
cssnano's multi-pass optimization produces significantly smaller output than simple whitespace removal. The default preset's most impactful optimizations are color shortening (converting #ffffff to #fff and RGB values to named colors), shorthand property merging (combining margin-top, margin-right, etc. into margin), and duplicate declaration removal. For a typical production web app, cssnano reduces CSS bundle size by 20-40% beyond what whitespace removal alone achieves. The advanced preset's zindex optimization (rebasing z-index values to sequential small integers) is powerful but dangerous in multi-team projects where z-index values carry semantic meaning — it's best disabled unless you fully control all CSS. Lightning CSS's minification matches cssnano's default preset in most cases and exceeds it for modern CSS features like color functions, where it can collapse oklch() values to shorter hex representations for older browser targets.
TypeScript in PostCSS Plugin Development
Writing PostCSS plugins in TypeScript has become the norm in 2026. PostCSS exports TypeScript types for all its AST node types, and the plugin interface is typed so your IDE provides autocomplete for node properties and methods. The postcss and postcss-load-config packages both ship TypeScript declarations. For plugin authors targeting npm publication, compile the plugin to CommonJS for broad compatibility (PostCSS is used in many build tools that require CJS) and include type declarations. A critical TypeScript consideration: PostCSS plugin development requires understanding the difference between synchronous and asynchronous plugin forms — PostCSS supports async plugins via the Once, OnceExit, Rule, and similar async visitor methods, but mixing sync and async plugins requires care to avoid ordering issues.
Ecosystem Trajectory and Tailwind v4 Impact
Tailwind CSS's architectural shift in v4 has significant implications for the PostCSS ecosystem. Tailwind v4 moves away from being a PostCSS plugin and instead provides its own dedicated Vite plugin and separate processing pipeline. This reduces PostCSS's role for teams that relied on PostCSS primarily as the mechanism for running Tailwind. However, PostCSS remains essential for non-Tailwind CSS processing, for projects on Tailwind v3, and for the enormous ecosystem of PostCSS plugins that have no equivalent in other tools. Lightning CSS's growth trajectory suggests it will become the default CSS transformer in Vite and other modern build tools over the next few years — but PostCSS's plugin ecosystem and the massive installed base ensure it remains relevant for at least five more years. For new projects in 2026 not using Tailwind, Lightning CSS is the forward-looking choice; for Tailwind v3 users, PostCSS remains required.
CSS Performance Budgets and Build Optimization
CSS bundle size and processing time contribute measurably to application performance. Unminified CSS in production is a surprisingly common oversight — developers who add cssnano to webpack or Vite configuration but then switch build tools sometimes lose the minification step. Audit your production CSS bundle size periodically: a React application's CSS should typically be under 50KB gzipped for good performance. The biggest CSS performance wins in 2026 come from removing unused styles — PurgeCSS (a PostCSS plugin) analyzes your HTML and JavaScript to identify which CSS selectors are actually used and removes the rest. Tailwind's JIT mode builds only used utility classes, making PurgeCSS unnecessary for Tailwind projects. For CSS authored by hand or using component libraries, unused selectors accumulate as features are removed or redesigned — periodic PurgeCSS audits prevent CSS bundles from growing unboundedly over a project's lifetime.
Compare CSS tooling and build utilities on PkgPulse →
Compare LightningCSS and PostCSS package health on PkgPulse.
See also: react-scan vs why-did-you-render vs Million Lint 2026 and perfect-debounce vs lodash.debounce vs throttle-debounce: Debounce Utilities in JavaScript 2026, html-minifier-terser vs htmlnano vs minify-html (2026).