Vite plugin for correct CSS Modules behavior
MIT License
This Vite plugin fixes several CSS Module bugs by handling CSS Modules as JS Modules.
The goal of this project is to incorporate this fix directly into Vite (PR #16018). Meanwhile, this plugin is published for early adopters and users who are unable to upgrade Vite.
→ Play with a demo on StackBlitz
Handle CSS Modules as JS Modules
The plugin changes how Vite processes CSS Modules. Currently, Vite bundles each CSS Module entry-point separately using postcss-modules.
By treating them as JavaScript modules, they can now be integrated into Vite's module graph, allowing Vite to handle the bundling. This allows:
Improved error handling
Currently, Vite (or more specifically, postcss-modules
) fails silently when unable to resolve a composes
dependency. This plugin will error on missing exports, helping you catch CSS bugs earlier on. (fix #16075)
For more details, see the FAQ below.
npm install -D vite-css-modules
In vite.config.js
:
import { patchCssModules } from 'vite-css-modules'
export default {
plugins: [
patchCssModules()
// Other plugins
],
css: {
// Configure CSS Modules as you previously did
modules: {
// ...
},
// Or LightningCSS
lightningcss: {
cssModules: {
// ...
}
}
},
build: {
// Recommended minimum is `es2022` so we can take advantage of new ESM features
target: 'esnext'
}
}
This patches your Vite to handle CSS Modules in a more predictable way.
Vite uses postcss-modules
to create a separate bundle for each CSS Module entry point, which leads to the following problems:
CSS Modules are not integrated with the Vite build
postcss-modules
bundles each CSS Module entry-point in a black-box, preventing Vite plugins from accessing any of the dependencies it resolves. This effectively limits further CSS post-processing from Vite plugins (e.g. SCSS, PostCSS, or LightningCSS).
Although postcss-modules
tries to apply other PostCSS plugins to dependencies, it seems to have issues:
Duplicated CSS Module dependencies
Since each CSS Module is bundled separately at each entry-point, dependencies shared across those entry-points are duplicated in the final Vite build.
This leads to bloated final outputs, and the duplicated composed classes can disrupt the intended style by overriding previously declared classes.
Inherently, CSS Modules are CSS files with a JavaScript interface exporting the class names.
This plugin preserves their nature and compiles each CSS Module into an JS module that loads the CSS. In each CSS Module, the composes
are transformed into JavaScript imports, and the exports consist of the class names. This allows Vite (or Rollup) to efficiently resolve, bundle, and de-duplicate the CSS Modules.
This process mirrors the approach taken by Webpack’s css-loader
, making it easier for those transitioning from Webpack. And since this technically does less work to load CSS Modules, I'm sure it's marginally faster in larger apps.
The patch disables Vite's default CSS Modules behavior, injects this plugin right before Vite's vite:css-post
plugin, and patches the vite:css-post
plugin to handle the JS output from the plugin.
In previous versions of ES, named exports only allowed names that could be represented as valid JavaScript variables, excluding names with special characters. This meant some class names (e.g. containing hyphens .foo-bar
) were not directly exportable as named exports, though they could be included in the default export object.
But in ES2022, the spec added support for exporting & importing names with any characters, including hyphens, by representing them as strings (https://github.com/tc39/ecma262/pull/2154). This allows class names like .foo-bar
to be directly exported as named exports in ES2022 or later versions.
To get access all class names as named exports, set your Vite config build.target
to es2022
or above and import them as follows:
import { 'foo-bar' as fooBar } from './styles.module.css'