A PostCSS plugin to use CSS Modules everywhere. Not only at the client side.
For example, you have the following CSS:
/* styles.css */
:global .page {
padding: 20px;
}
.title {
composes: title from "./mixins.css";
color: green;
}
.article {
font-size: 16px;
}
/* mixins.css */
.title {
color: black;
font-size: 40px;
}
.title:hover {
color: red;
}After the transformation it will become like this:
._title_116zl_1 {
color: black;
font-size: 40px;
}
._title_116zl_1:hover {
color: red;
}
.page {
padding: 20px;
}
._title_xkpkl_5 {
color: green;
}
._article_xkpkl_10 {
font-size: 16px;
}And the plugin will give you a JSON object for transformed classes:
{
"title": "_title_xkpkl_5 _title_116zl_1",
"article": "_article_xkpkl_10"
}By default, a JSON file with exported classes will be placed next to corresponding CSS.
But you have a freedom to make everything you want with exported classes, just
use the getJSON callback. For example, save data about classes into a corresponding JSON file:
postcss([
require("postcss-modules")({
getJSON: function (cssFileName, json, outputFileName) {
var path = require("path");
var cssName = path.basename(cssFileName, ".css");
var jsonFileName = path.resolve("./build/" + cssName + ".json");
fs.writeFileSync(jsonFileName, JSON.stringify(json));
},
}),
]);getJSON may also return a Promise.
import styles from "./x.css" is already supported by every major bundler — they invoke postcss-modules (or its building blocks) under the hood:
- webpack —
css-loaderwith{ modules: true } - Vite — built-in, files named
*.module.css - esbuild —
esbuild-css-modules-plugin - Rollup —
rollup-plugin-postcsswithmodules: true
postcss-modules ships a Node module-customization hook at the postcss-modules/loader subpath. Register it with --import and any import of a .css file is intercepted, run through the plugin, and exposed as a default-exported token map.
node --import postcss-modules/loader app.mjs// app.mjs
import styles from "./button.css";
const el = document.createElement("div");
el.className = styles.primary; // "_button__primary_xkpkl_5"Composing across files (composes: foo from "./other.css") works without extra configuration — the same FileSystemLoader the PostCSS plugin uses resolves siblings.
The hook accepts the same options object as the PostCSS plugin
(generateScopedName, localsConvention, scopeBehaviour, globalModulePaths, exportGlobals, hashPrefix, Loader, resolve, root). Options are discovered, in priority order:
process.env.POSTCSS_MODULES_CONFIG— absolute or cwd-relative path to a config file.postcss-modules.config.{js,cjs,mjs}inprocess.cwd().- A
"postcss-modules"key in the nearestpackage.json. - Empty defaults.
Example config file:
// postcss-modules.config.cjs
module.exports = {
generateScopedName: "[name]__[local]___[hash:base64:5]",
localsConvention: "camelCaseOnly",
};Or inline in package.json:
{
"postcss-modules": {
"generateScopedName": "[name]__[local]___[hash:base64:5]"
}
}Or via an environment variable, useful for per-environment overrides:
POSTCSS_MODULES_CONFIG=./config/css-modules.cjs node --import postcss-modules/loader app.mjsBoth runners accept the same --import flag through their Node options.
NODE_OPTIONS="--import postcss-modules/loader" jest
NODE_OPTIONS="--import postcss-modules/loader" vitestOr wire it into a setup file. The loader runs once per worker, so spawned worker pools (Jest's default) pick it up automatically.
Add an ambient declaration so the compiler accepts default-importing a .css file:
// css-modules.d.ts
declare module "*.css" {
const tokens: { readonly [key: string]: string };
export default tokens;
}- ESM only.
require("./x.css")is not supported; the underlying transform is async and cannot be expressed throughrequire.extensions. Use a.mjsentrypoint, or"type": "module"inpackage.json. - Tokens only, no CSS output. The loader returns the class-name map but does not emit the transformed CSS. SSR setups that need the stylesheet should use a bundler or call
postcss([require("postcss-modules")(...)]).process(...)directly to capture both. - No HMR, no watch mode — by design. Use a bundler for those.
- Node ≥ 20.6.
module.registerstabilized there.
By default, the plugin assumes that all the classes are local. You can change
this behaviour using the scopeBehaviour option:
postcss([
require("postcss-modules")({
scopeBehaviour: "global", // can be 'global' or 'local',
}),
]);To define paths for global modules, use the globalModulePaths option.
It is an array with regular expressions defining the paths:
postcss([
require('postcss-modules')({
globalModulePaths: [/path\/to\/legacy-styles/, /another\/paths/],
});
]);To generate custom classes, use the generateScopedName callback:
postcss([
require("postcss-modules")({
generateScopedName: function (name, filename, css) {
var path = require("path");
var i = css.indexOf("." + name);
var line = css.substr(0, i).split(/[\r\n]/).length;
var file = path.basename(filename, ".css");
return "_" + file + "_" + line + "_" + name;
},
}),
]);Or just pass an interpolated string to the generateScopedName option
(More details here):
postcss([
require("postcss-modules")({
generateScopedName: "[name]__[local]___[hash:base64:5]",
}),
]);It's possible to add custom hash to generate more unique classes using the hashPrefix option (like in css-loader):
postcss([
require("postcss-modules")({
generateScopedName: "[name]__[local]___[hash:base64:5]",
hashPrefix: "prefix",
}),
]);If you need to export global names via the JSON object along with the local ones, add the exportGlobals option:
postcss([
require("postcss-modules")({
exportGlobals: true,
}),
]);If you need, you can pass a custom loader (see FileSystemLoader for example):
postcss([
require("postcss-modules")({
Loader: CustomLoader,
}),
]);You can also pass any needed root path:
postcss([
require('postcss-modules')({
root: 'C:\\',
});
]);Type: String | (originalClassName: string, generatedClassName: string, inputFile: string) => string | string[]
Default: null
Style of exported classnames, the keys in your json.
| Name | Type | Description |
|---|---|---|
'camelCase' |
{String} |
Class names will be camelized, the original class name will not to be removed from the locals |
'camelCaseOnly' |
{String} |
Class names will be camelized, the original class name will be removed from the locals |
'dashes' |
{String} |
Only dashes in class names will be camelized |
'dashesOnly' |
{String} |
Dashes in class names will be camelized, the original class name will be removed from the locals |
'all' |
{String} |
Emit the original class name plus the camelCase and dashes-camelCase variants |
'none' |
{String} |
Emit only the original class name (equivalent to leaving localsConvention unset) |
For an input class foo-bar resolving to scoped name _a:
| Mode | Emitted locals |
|---|---|
camelCase |
{ "foo-bar": "_a", "fooBar": "_a" } |
camelCaseOnly |
{ "fooBar": "_a" } |
dashes |
{ "foo-bar": "_a", "fooBar": "_a" } |
dashesOnly |
{ "fooBar": "_a" } |
all |
{ "foo-bar": "_a", "fooBar": "_a" } (camelCase + dashesCamelCase collapse for this input) |
none |
{ "foo-bar": "_a" } |
In lieu of a string, a custom function can generate the exported class names. The function may return either a single string or an array of strings; when an array is returned every entry is added to the locals map and resolves to the same value:
postcss([
require("postcss-modules")({
localsConvention: (original, generated) => [original, generated, original.toUpperCase()],
}),
]);
// .foo-bar { ... } → { "foo-bar": "_a", "_a": "_a", "FOO-BAR": "_a" }You can rewrite paths for composes/from by using the resolve option.
It's useful when you need to resolve custom path alias.
Parameters:
file— a module we want to resolveimporter— the file that imports the module we want to resolve
Return value: string | null | Promise<string | null>
postcss([
require("postcss-modules")({
resolve: function (file, importer) {
return path.resolve(
path.dirname(importer),
file.replace(/^@/, process.cwd()
);
},
}),
]);The plugin only transforms CSS classes to CSS modules. But you probably want to integrate these CSS modules with your templates. Here are some examples.
Assume you've saved project's CSS modules in cssModules.json:
{
"title": "_title_xkpkl_5 _title_116zl_1",
"article": "_article_xkpkl_10"
}Let's say you have a Pug template about.jade:
h1(class=css.title) postcss-modules
article(class=css.article) A PostCSS plugin to use CSS Modules everywhereRender it:
var jade = require("jade");
var cssModules = require("./cssModules.json");
var html = jade.compileFile("about.jade")({ css: cssModules });
console.log(html);And you'll get the following HTML:
<h1 class="_title_xkpkl_5 _title_116zl_1">postcss-modules</h1>
<article class="_article_xkpkl_10">A PostCSS plugin to use CSS Modules everywhere</article>For HTML transformation we'll use PostHTML and PostHTML CSS Modules:
npm install --save posthtml posthtml-css-modulesHere is our template about.html:
<h1 css-module="title">postcss-modules</h1>
<article css-module="article">A PostCSS plugin to use CSS Modules everywhere</article>Transform it:
var fs = require("fs");
var posthtml = require("posthtml");
var posthtmlCssModules = require("posthtml-css-modules");
var template = fs.readFileSync("./about.html", "utf8");
posthtml([posthtmlCssModules("./cssModules.json")])
.process(template)
.then(function (result) {
console.log(result.html);
});(for using other build systems please check the documentation of PostHTML)
And you'll get the following HTML:
<h1 class="_title_xkpkl_5 _title_116zl_1">postcss-modules</h1>
<article class="_article_xkpkl_10">A PostCSS plugin to use CSS Modules everywhere</article>Sure! Take a look at the example.
See PostCSS docs for examples for your environment and don't forget to run
npm install --save-dev postcss postcss-modules
- Dmitry Olyenyov