Babel plugin for advanced CSS modules support in React:
- It transforms
styleName
attribute of JSX components intoclassName
using compile-time CSS module resolution, allowing for a cleaner use of CSS modules in React. - For server-side rendering (SSR) scenarios it can replace named stylesheet imports by classname mapping objects, and remove anonymous stylesheet imports.
Assuming style.css
in the following examples is compiled as CSS Module.
Without this plugin
import S from './styles.css';
export default function Component() {
return (
<div className={S.container}>
<h1 className={S.title}>Example</div>
<p styleName={S.text}>Sample text paragraph.</p>
<p className={`${S.text} ${S.special}`}>
Sample text paragraph with special style.
</p>
</div>
);
}
With this plugin
import './styles.css';
export default function Component() {
return (
<div styleName="container">
<h1 styleName="title">Example</div>
<p styleName="text">Sample text paragraph.</p>
<p styleName="text special">
Sample text paragraph with special style.
</p>
</div>
);
}
With this plugin and multiple stylesheets
Assuming:
- Styles
container
,title
, andtext
are defined instyles-01.css
. - Style
special
is defined instyles-02.css
. - The plugin's
autoResolveMultipleImports
option is enabled (default).
import './styles-01.css';
import './styles-02.css';
export default function Component() {
return (
<div styleName="container">
<h1 styleName="title">Example</div>
<p styleName="text">Sample text paragraph.</p>
<p styleName="text special">
Sample text paragraph with special style.
</p>
</div>
);
}
If both files, styles-01.css
and styles-02.css
contain styles with the same
names, thus making auto resolution impossible, this plugin allows explicit
stylesheet prefixes:
import S1 from './styles-01.css';
import S2 from './styles-02.css';
export default function Component() {
return (
<div styleName="S1.container">
<h1 styleName="S1.title">Example</div>
<p styleName="S1.text">Sample text paragraph.</p>
<p styleName="S1.text S2.special">
Sample text paragraph with special style.
</p>
</div>
);
}
With this plugin and runtime resolution
import './styles-01.css';
import './styles-02.css';
export default function Component({ special }) {
let textStyle = 'text';
if (special) textStyle += ' special';
return (
<div styleName="container">
<h1 styleName="title">Example</div>
<p styleName={textStyle}>Sample text paragraph.</p>
<p styleName={textStyle}>
Sample text paragraph with special style.
</p>
</div>
);
}
In the case when the exact style value is not known at the compile time, like in
this example, the plugin will inject necessary code to correctly resolve the
styleName
at runtime (which is somewhat less performant, but otherwise works
fine).
SSR scenario
Consider such component, which uses a named stylesheet import in order to use it in some other ways, beside simple styling, e.g. to also display the classname mapping:
import S from './style.css';
export default function Component() {
return (
<div styleName="container">
{JSON.stringify(S)}
</div>
)
}
While by default this plugin transforms it into (leaving it to the Webpack's
css-loader to handle ./style.scss
import for the actual CSS bundling,
and leaving a correct JS in place of it):
import S from './style.css';
export default function Component() {
return (
<div className="12345">
{JSON.stringify(S)}
</div>
)
}
For server-side environment, if you don't compile server-side code with Webpack,
you'll need to replace ./style.css
with valid JS code. That is exactly what
this plugin does with replaceImport
option enabled, it outputs:
const S = {
container: '12345',
// Other stylesheet keys, if any.
};
export default function Component() {
return (
<div className="12345">
{JSON.stringify(S)}
</div>
)
}
CommonJS require() support
The plugin works the same with require('./style.css')
CSS imports.
-
The core CSS Modules functionality should be enabled and configured elsewhere in your React project:
- With Create React App see Adding a CSS Modules Stylesheet.
- With bare Webpack see
modules
option ofcss-loader
. - With other frameworks refer to their documentation.
-
Install this plugin as a direct dependency (in edge-cases not allowing for a compile-time
styleName
resolution, the plugin falls back to the runtime resolution).npm install --save @dr.pogodin/babel-plugin-react-css-modules
-
Install Webpack at least as a dev dependency:
npm install --save-dev webpack
-
Add the plugin to Babel configuration:
{ "plugins": [ ["@dr.pogodin/react-css-modules", { // The default localIdentName in "css-loader" is "[hash:base64]", // it is highly-recommended to explicitly specify the same value // both here, and in "css-loader" options, including the hash length // (the last digit in the template below). "generateScopedName": "[hash:base64:6]" // See below for all valid options. }] ] }
-
The
generateScopedName
option value MUST matchlocalIdentName
option ofcss-loader
to ensure both Webpack and this plugin generate matching class names. The same goes for other options impacting class names (e.g. the default length of hashes generated by Webpack, which is used if you don't specify the hash length explicitly inlocalIdentName
hash placeholders), and also the actuals version of this plugin andcss-loader
(seecss-loader
compatibility). -
Optional.
css-loader
is known for eventual minor updates in their default class name generation logic that require counterpart upgrades of this plugin to keep it compatible. They denied to expose the default class name generator for re-used by 3rd party libraries, and suggested to rely ongetLocalIdent
option if unwanted class name changes due tocss-loader
updates are a problem for a particular project.To alleviate this issue, this plugin provides stable default implementation for
getLocalIdent
function (taken from a selected earlier version ofcss-loader
). Consider to use it:Within Webpack Config
const { getLocalIdent } = require('@dr.pogodin/babel-plugin-react-css-modules/utils'); const cssLoaderOptions = { modules: { getLocalIdent, localIdentName: '[path]___[name]__[local]___[hash:base64:6]' } };
Within Babel Config
const { generateScopedNameFactory } = require('@dr.pogodin/babel-plugin-react-css-modules/utils'); module.exports = { plugins: [ ["@dr.pogodin/react-css-modules", { generateScopedName: // The classname template MUST match "localIdentName" option value // you passed to "css-loader". generateScopedNameFactory("[path]___[name]__[local]___[hash:base64:6]"), }] ] };
In addition to the standard class name template placeholders mentioned in
css-loader
documentation the version ofgetLocalIdent()
andgenerateScopedName()
provided by this plugin also support[package]
placeholder. If used, it looks up from CSS file for the closestpackage.json
file, takes the package name from it, and inserts it into the class name (this is useful for CSS bundling for libraries).
If you'd like to get this working in React Native, you're going to have to allow
custom import extensions, via a rn-cli.config.js
file:
module.exports = {
getAssetExts() {
return ["scss"];
}
}
Remember, also, that the bundler caches things like plugins and presets. If you
want to change your .babelrc
(to add this plugin) then you'll want to add the
--reset-cache
flag to the end of the package command.
These are valid plugin options. All are optional, but the overall configuration
should be compatible with that of css-loader
, thus defaults may not work for
you.
-
context
- string - Must match webpackcontext
configuration.css-loader
inheritscontext
values from webpack. Other CSS module implementations might use different context resolution logic. Defaultsprocess.cwd()
. -
exclude
- string - A RegExp that will exclude otherwise included files e.g., to exclude all styles from node_modules:exclude: 'node_modules'
. -
filetypes
- Configurate syntax loaders like sugarss, LESS and SCSS, and extra plugins for them. -
generateScopedName
- function | string - Allows to customize the exactstyleName
toclassName
conversion algorithm. For details see Generating scoped names. Defaults[path]___[name]__[local]___[hash:base64:5]
. -
replaceImport
- boolean - Replaces / removes stylesheet imports for server-side rendering purposes. See details below. Defaults false. -
webpackHotModuleReloading
- boolean |"commonjs"
- Enables injection of Hot Module Reloading code. -
handleMissingStyleName
- string - Determines what should be done for undefined CSS modules (using astyleName
for which there is no CSS module defined). Valid values:"throw"
,"warn"
,"ignore"
. Setting this option to"ignore"
is equivalent to settingerrorWhenNotFound: false
in react-css-modules. Defaults"throw"
. -
attributeNames
- Custom Attribute Mapping -
skip
- boolean - Whether to apply plugin if no matchingattributeNames
found in the file. Defaults false. -
transform - function - If provided, each CSS source loaded by the plugin will be passed through this function, alongside its path, and this plugin's options, and the output of this function will be used in place of the original CSS.
-
autoResolveMultipleImports
- boolean - Allows multiple anonymous imports ifstyleName
is only in one of them. Defaults true.
- UseremoveImport
- booleanreplaceImport
option instead.
To add support for different CSS syntaxes (e.g. SCSS), perform the following two steps:
-
Add the postcss syntax loader as a development dependency:
npm install postcss-scss --save-dev
-
Add a
filetypes
syntax mapping to the Babel plugin configuration. For example for SCSS:"filetypes": { ".scss": { "syntax": "postcss-scss" } }
And optionally specify extra plugins:
"filetypes": { ".scss": { "syntax": "postcss-scss", "plugins": [ "postcss-nested" ] } }
NOTE:
postcss-nested
is added as an extra plugin for demonstration purposes only. It's not needed withpostcss-scss
because SCSS already supports nesting.Postcss plugins can have options specified by wrapping the name and an options object in an array inside your config:
"plugins": [ ["postcss-import-sync2", { "path": ["src/styles", "shared/styles"] }], "postcss-nested" ]
If you don't know what is Hot Module Reloading (HMR), refer to the Webpack documentation.
If you use HMR in your development setup (you probably should), depending on
your particular configuration you might need to enable webpackHotModuleReloading
option of this plugin, or you may need to leave it disabled (default), as other
loaders / plugins in your Webpack pipeline for CSS may already inject required
HMR code.
In case you decide to enable it in this plugin, webpackHotModuleReloading
option may be set equal:
- true - this plugin will inject HMR accept code for each imported CSS
module, using
import.meta.webpackHot
(ESM) syntax (see for details). commonjs
string - this plugin will inject HMR accept code using the legacymodule.hot
syntax.
The default value is false - this plugin does not inject HMR accept code.
function transform(cssSource, cssSourceFilePath, pluginOptions): string
The transform function, if provided as the transform
option of this plugin,
will be called for each loaded CSS source with three arguments:
cssSource
- string - The loaded CSS code.cssSourceFilePath
- string - The path of loaded CSS file.pluginOptions
- object - The options set for this plugin.
It should return a string, the actual CSS code to use.
You can set your own attribute mapping rules using the attributeNames
option.
It's an object, where keys are source attribute names and values are destination attribute names.
For example, the
<NavLink>
component from React Router
has an activeClassName
attribute to accept an additional class name. You can
set "attributeNames": { "activeStyleName": "activeClassName" }
to transform it.
The default styleName
-> className
transformation will not be affected
by an attributeNames
value without a styleName
key. Of course you can use
{ "styleName": "somethingOther" }
to change it, or use { "styleName": null }
to disable it.
If replaceImport
flag is set, this plugin will remove or replace original
stylesheet imports, which is needed for server-side rendering:
// Anonymous imports are removed from the code:
import 'path/to/style.css';
// Default and named imports are replaced in the following manner:
// Before:
import styles, {
className,
otherClassName as alias,
} from 'path/to/style.css';
// After:
const styles = {
className: 'generatedClassName',
otherClassName: 'otherGeneratedClassName',
},
className = 'generatedClassName',
alias = 'otherGeneratedClassName';
// Also this kind of import:
import * as style from 'path/to/style.css';
// is replaced by:
const style = {
className: 'generatedClassName',
otherClassName: 'otherGeneratedClassName',
};
This plugin does the following:
- Builds index of all stylesheet imports per file (imports of files with
.css
or.scss
extension). - Uses postcss to parse the matching CSS files into a lookup of CSS module references.
- Iterates through all JSX element declarations.
- Parses the
styleName
attribute value into anonymous and named CSS module references. - Finds the CSS class name matching the CSS module reference:
- If
styleName
value is a string literal, generates a string literal value. - If
styleName
value is ajSXExpressionContainer
, uses a helper function (getClassName
) to construct theclassName
value at the runtime.
- If
- Removes the
styleName
attribute from the element. - Appends the resulting
className
to the existingclassName
value (createsclassName
attribute if one does not exist).
This plugin is an up-to-date, well-maintained fork of the original
babel-plugin-react-css-modules
:
- It generates class names matching current
css-loader
versions (seecss-loader
compatibility for details). - All dependencies are upgraded to the latest versions.
- Follow-up maintenance and improvements are performed as necessary.
The original babel-plugin-react-css-modules
plugin is largely abandoned by
its author since March 2019. When an year later updates of css-loader
and
Webpack broke dependant projects, with no reaction from
babel-plugin-react-css-modules
author on emerging issue reports in GitHub,
I (birdofpreyru) created this fork to ensure stability of my own projects
relying on it.
I am banned from commenting in the original project repo since I tried a little self-promo, trying to encourage people to switch over to my fork. If you read this, consider to spread the word to encourage more users to move to this fork.
-
Prefix plugin name in your Babel config by
@dr.pogodin/
scope, i.e.:@dr.pogodin/babel-plugin-react-css-modules
or@dr.pogodin/react-css-moudles
instead ofbabel-plugin-react-css-modules
orreact-css-modules
. -
Be sure to have
webpack
installed (it is a must-to-have peer dependency of this plugin starting fromv6.2.0
).
css-loader versions |
this plugin versions |
---|---|
7.0.0 – 7.1.2 (latest) |
6.13.0 – 6.13.2 (latest) |
6.7.1 – 6.11.0 |
6.7.0 – 6.12.0 |
6.5.0 – 6.7.0 |
6.5.1 – 6.6.1 |
6.4.0 |
6.4.0 – 6.4.1 |
6.0.0 – 6.3.0 |
6.2.1 – 6.3.1 |
5.2.5 – 5.2.7 |
6.1.1 |
5.2.4 |
6.1.0 |
5.1.3 – 5.2.3 |
6.0.11 / 6.1.0 (1) |
5.0.0 – 5.1.2 |
6.0.7 – 6.0.11 |
4.2.0 – 4.3.0 |
6.0.3 – 6.0.6 |
≤ 3.6.0 |
original plugin |
1) There might be some corner-case differences in class name transformation between these versions of css-loader
and this plugin, but most probably they won't break compatibility for most users.