
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
vite-css-modules
Advanced tools
Vite plugin to fix broken CSS Modules handling.
→ Play with a demo on StackBlitz
[!NOTE] We're working to integrate this fix directly into Vite (PR #16018). Until then, use this plugin to benefit from these improvements now.
Have you encountered any of these Vite CSS Module bugs? They're happening because Vite's CSS Modules implementation delegates everything to postcss-modules, creating a black box that Vite can't see into.
This plugin fixes these issues by properly integrating CSS Modules into Vite's build pipeline.
composes dependenciesWhen you use composes to import classes from another CSS file, Vite's PostCSS plugins never process the imported file. This means your PostCSS transformations, auto-prefixing, or custom plugins are silently skipped for dependencies.
What happens in Vite:
style.module.css gets processed by PostCSS ✓utils.css does NOT get processed by PostCSS ✗utils.css because postcss-modules bundles it internallyWith this plugin:
When multiple CSS Modules compose from the same utility file, the utility's styles get bundled multiple times. This increases bundle size and can cause style conflicts.
Example:
/* utils.css */
.button { padding: 10px; }
/* header.module.css */
.title { composes: button from './utils.css'; }
/* footer.module.css */
.link { composes: button from './utils.css'; }
What happens in Vite:
/* Final bundle contains .button styles TWICE */
.button { padding: 10px; } /* from header.module.css */
.button { padding: 10px; } /* from footer.module.css - duplicated! */
With this plugin:
/* Final bundle contains .button styles ONCE */
.button { padding: 10px; } /* deduplicated */
composes classes fail silentlyIf you typo a class name in composes, Vite doesn't error. Instead, it outputs undefined in your class names, breaking your UI with no warning.
Example:
.button {
composes: nonexistant from './utils.css'; /* Typo! */
}
What happens in Vite:
import styles from './style.module.css'
console.log(styles.button) // "_button_abc123 undefined" - no error!
With this plugin:
Error: Cannot find class 'nonexistent' in './utils.css'
Trying to compose from SCSS/Sass files causes syntax errors because postcss-modules tries to parse SCSS as plain CSS.
Example:
/* base.module.scss */
.container { display: flex; }
/* style.module.css */
.wrapper { composes: container from './base.module.scss'; }
What happens in Vite:
CssSyntaxError: Unexpected '/'
With this plugin:
Changing a CSS Module file causes a full page reload instead of a hot update, losing component state.
What happens in Vite:
With this plugin:
Using JavaScript reserved keywords as class names (like .import, .export) generates invalid JavaScript code.
Example:
.import { color: red; }
.export { color: blue; }
What happens in Vite:
// Tries to generate invalid JavaScript:
export const import = "..."; // Syntax error "import" is reserved!
export const export = "..."; // Syntax error "export" is reserved!
With this plugin:
npm install -D vite-css-modules
In your Vite config file, add the patchCssModules() plugin to patch Vite's CSS Modules behavior:
// vite.config.js
import { patchCssModules } from 'vite-css-modules'
export default {
plugins: [
patchCssModules() // ← This is all you need to add!
// Other plugins...
],
css: {
// Your existing CSS Modules configuration
modules: {
// ...
},
// Or if using LightningCSS
lightningcss: {
cssModules: {
// ...
}
}
},
build: {
// Recommended minimum target (See FAQ for more details)
target: 'es2022'
}
}
This patches your Vite to handle CSS Modules in a more predictable way.
Configuring the CSS Modules behavior remains the same as before.
Read the Vite docs to learn more.
This plugin can generate type definition (.d.ts) files for CSS Modules, providing autocomplete and type checking for class names. For example, importing style.module.css will create a style.module.css.d.ts next to it:
patchCssModules({
generateSourceTypes: true
})
Add declarationMap to include inline source maps in the generated .d.ts files:
patchCssModules({
generateSourceTypes: true,
declarationMap: true
})
This enables "Go to Definition" (F12 / Cmd+Click in VS Code) to jump from a CSS class name in TypeScript directly to where it's defined in the CSS file. Without declaration maps, "Go to Definition" lands on the generated .d.ts — a machine-generated file with no useful context.
When not explicitly set, declarationMap auto-detects from tsconfig.json's compilerOptions.declarationMap.
patchCssModules(options)exportMode'both' | 'named' | 'default''both'Specifies how class names are exported from the CSS Module:
both: Exports class names as both named and default exports.named: Exports class names as named exports only.default: Exports class names as a default export only (an object where keys are class names).generateSourceTypesbooleanfalseGenerates a .d.ts file next to each CSS Module with type definitions for the exported class names.
declarationMapbooleantsconfig.json's compilerOptions.declarationMapGenerates inline declaration source maps in .d.ts files, enabling "Go to Definition" to navigate from TypeScript to CSS source. Requires generateSourceTypes to be enabled.
[!TIP] Source maps are always inlined rather than emitted as separate
.d.ts.mapfiles. Since.d.tsfiles are generated in-place next to your CSS source, external map files would pollute the source directory. The size overhead of inlining is negligible for typical CSS modules.
Generate TypeScript declarations for CSS Modules without running a build.
npx vite-css-modules [globs...] [--config path] [--mode mode]
The CLI expects a vite.config.* file in the current working directory and uses that one config for every matched file.
With no globs, the CLI defaults to:
**/*.module.{css,scss,sass}
and searches under the resolved Vite root.
When you pass globs explicitly, they are resolved from the current working directory instead.
Run it from the same cwd you would use for vite, or pass --config to point at a specific config file. Use --mode when your Vite config depends on the active mode.
The CLI requires patchCssModules() in your Vite config and reads its options (exportMode, declarationMap) along with Vite's css.modules settings.
| Flag | Description |
|---|---|
--config <path> | Use a specific vite.config.* file |
--mode <mode> | Set the Vite mode used when loading config |
# Generate types for CSS Modules under the Vite root
npx vite-css-modules
# Use explicit globs from the current working directory
npx vite-css-modules 'src/**/*.module.css'
# Use a specific Vite config
npx vite-css-modules 'src/**/*.module.css' --config apps/web/vite.config.ts
# Load config with production mode
npx vite-css-modules 'src/**/*.module.css' --mode production
Vite delegates bundling each CSS Module to postcss-modules, leading to significant problems:
CSS Modules not integrated into Vite's build
Since postcss-modules is a black box that only returns the final bundled output, Vite plugins can't hook into the CSS Modules build or process their internal dependencies. This prevents post-processing by plugins like SCSS, PostCSS, or LightningCSS. (#10079, #10340)
Duplicated CSS Module dependencies
Bundling CSS Modules separately duplicates shared dependencies, increasing bundle size and causing style overrides. (#7504, #15683)
Silent failures on unresolved dependencies
postcss-modules fails silently when it can't resolve a composes dependency—missing exports don't throw errors, making CSS bugs harder to catch. (#16075)
Crash on circular composes dependencies
When two CSS Modules compose from each other, postcss-modules triggers a deadlock that crashes the build with an unhelpful internal error. This plugin detects the cycle and throws a descriptive error showing the dependency chain (e.g. Circular CSS Module dependency: "a.module.css" -> "b.module.css" -> "a.module.css").
The vite-css-modules plugin fixes these issues by seamlessly integrating CSS Modules into Vite's build process.
The plugin treats CSS Modules as JavaScript modules, fully integrating them into Vite's build pipeline. Here's how:
Transforms CSS into JS modules
CSS Modules are compiled into JS files that load the CSS. composes rules become JS imports, and class names are exported as named JS exports.
Integrates with Vite's module graph
Because they're now JS modules, CSS Modules join Vite's module graph. This enables proper dependency resolution, bundling, and de-duplication.
Unlocks plugin compatibility
Other Vite plugins can now access and process CSS Modules—fixing the prior limitation where dependencies inside them were invisible.
This model is similar to Webpack's css-loader, making it familiar to devs transitioning from Webpack. It also reduces overhead and improves performance in larger projects.
Yes, but there are a few things to keep in mind:
JavaScript naming restrictions
Older JavaScript versions don't allow special characters (like -) in variable names. So a class like .foo-bar couldn't be imported as foo-bar and had to be accessed via the default export.
Using localsConvention
To work around this, set css.modules.localsConvention: 'camelCase' in your Vite config. This converts foo-bar → fooBar, making it a valid named export.
ES2022 support for arbitrary names
With ES2022, you can now export/import names with any characters using quotes. This means .foo-bar can be used as a named export directly.
To enable this, set your build target to es2022:
// vite.config.js
export default {
build: {
target: 'es2022'
}
}
Then import using:
import { 'foo-bar' as fooBar } from './styles.module.css'
// Use it
console.log(fooBar)
This gives you full named export access—even for class names with previously invalid characters.
FAQs
Vite plugin for correct CSS Modules behavior
The npm package vite-css-modules receives a total of 23,513 weekly downloads. As such, vite-css-modules popularity was classified as popular.
We found that vite-css-modules demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.