Updates
05/07/20 - The localIdentName
property for css-loader is now a sub-property of the modules
property.
02/10/18 - Looks like Create React App now adds support for CSS Modules
Quick Intro
CSS Modules are meant as a way to locally scope class and animation names to help prevent some of the downfalls of the default global scope that normal CSS is based on.
PostCSS provides a way to transform styles using JavaScript plugins.
Can we make them work together? Why would we want to? Let’s answer the second question first.
The Why
PostCSS has a deep, deep ecosystem, and of particular interest to me is the postcss-preset-env plugin. Using this plugin gets you access to the newest CSS features, and their polyfills, today. This role used to be filled by cssnext but this was recently deprecated, so what better time to explore postcss-preset-env?
An early issue I’ve had using CSS Modules has been its ‘variables’ syntax. While they definitely exist, I’m really not sold on the syntax, and CSS Variables are already in the spec so why are we re-inventing this particular wheel? I don’t think I’m alone in this feeling, other people seem to be asking how to use CSS Modules with the standard CSS Variables, so here we are.
Couple this with some fairly sweet future CSS functionality, and we’ve got reason enough to tie PostCSS together with CSS Modules.
The How
Alright lets get to it. I hope you’re ready for your node_modules directory to grow a fair bit, we’ve got some installing to do!
First up, getting CSS Modules to work at all in the context of your React application.
CSS Modules
Let’s get babel-plugin-react-css-modules (is there a longer npm package name?) installed. It has a runtime component, so it actually belongs in your dependencies rather than your devDependencies. Install it like this:
npm install babel-plugin-react-css-modules --save
Make sure your .babelrc or whatever equivalent you are using to configure Babel includes the new plugin:
plugins: [‘react-css-modules’]
And now we need to configure Webpack to tell it how to load in CSS files. We’ll need style-loader and css-loader for this one. These are regular devDependencies so make sure you install them as such.
npm install css-loader style-loader --save-dev
Now lets tell Webpack to use these loaders for CSS files. Add the following to your webpack.config.js
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[path]___[name]__[local]___[hash:base64:5]',
},
},
},
],
}
What’s up with that localIdentName? Good question! When you switch on the CSS Modules spec, css-loader will munge your css classes according to this ident. This means you can have two .button classes in your codebase and they wont conflict.
However this means that when you add a class name to your React component you’d need to know what css-loader is going to transform your class names into, right? Well that’s where babel-plugin-react-css-modules comes in. It’ll do the same munging of class names as css-loader, we just have to make sure they are configured to use the same method of munging.
The default value for this option in css-loader is different to babel-plugin-react-css-modules, so specifying it to be [path]__[name][local]__[hash:base64:5] fixes that.
Great, now in your React component you should be able to import the CSS file directly:
App.css
.app {
border: 1px solid red;
}
App.jsx
import React from 'react';
import './App.css';
const App = () => (
<div styleName="app">
Hello, world!
</div>
);
export default App;
The styleName property is babel-plugin-react-css-modules’s replacement for className, but you get used to it pretty quickly.
Assuming everything has worked, you’ll have class names that look like word soup:
PostCSS
And now for the fun stuff. Lots of modules to install, so lets get started:
npm install postcss postcss-import postcss-loader postcss-preset-env postcss-url --save-dev
We will need to change our webpack.config.js to make sure the postcss-loader gets used:
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[path]___[name]__[local]___[hash:base64:5]',
},
},
},
{ loader: 'postcss-loader' }
],
}
And now we need a new postcss.config.js file
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-url'),
require('postcss-preset-env')({
browsers: 'last 2 versions',
stage: 0,
}),
],
};
Now we can try it out! Make a new colors.css file
:root {
--errorRed: #e03030;
}
and change App.css to use it:
@import "../colors.css";
.app {
border: 1px solid var(--errorRed);
}
How’s that style block looking?
Nice! You get the var() rule for browsers that support it, or the fallback for those that don’t.
Wrapping Up
Getting the right combination of modules to make this work was the real challenge on this one, the configuration itself was fairly easy.
If this doesn’t work for you, something is missing, or you think I’ve gone about this the wrong way, I’d love to hear from you in the comments.
Top comments (13)
I really love that I found your article which still seems to work (these things are so damn timely haha). I do find the ident thingy super duper frustrating. I'm utilizing css modules
composes
and for just two classesbtn btn-primary
it's resulting insrc-components-___base__button___3KDTY src-components-___base__btn___1Fsux
which is ridonkulous to read.Also, I don't know that I understand the whole
styleName
thing. Is that required? I seem to be fine leavingclassName
.Anyway, thanks for the step by step!
See my other comments for more on styleName, but you don't have to use it, it can exist alongside className or be ignored entirely. :)
Gotcha thanks
Hi,
This is a great tutorial, but I think you have an issue in your code. You see, you can't use postcss-import with CSS modules without creating a whole bunch of duplicates. Every file in which you import your colors.css gets inlined which creates a bunch of duplicate :root statements in your final CSS.
You can see more in this issue: github.com/postcss/postcss-import/...
Hi, thanks for the feedback! Yes it looks like duplicate roots are definitely an issue, unfortunately as far as I can tell there is no current way around it?
There isn't an official or suggested way around it, as far as I know, while I was dealing with this problem. But there are some workarounds. I'm using postcss-preset-env and there's an importFrom option. There you can load a file with variables which are going to be provided for each file, so there's no need to import them manually.
Downside is that you can only import .css, .js or .json. Which is unfortunate if you're using something like SASS, LESS or simply different syntax like SugarSS. So you have to keep variables in different file type than all the others. When it comes to mixins, they don't get injected so I import them per file.
Another possibility is to use one of the deduplication plugins provided.
Last one I can think of is just use postcss-simple-vars, instead of :root and native CSS variables.
Although I was using variables like this plugin supports, I like to use native CSS variables when they're supported, even though I like plugin syntax better.
Hope this helps as bit.
So
styleName
comes from thebabel-plugin-react-css-modules
plugin. Stolen from their docs:However, there are several several disadvantages of using CSS modules this way:
Using
babel-plugin-react-css-modules
:Hi,
Thanks for the great tutorial.
I have the configurations as explained in the tutorial but still css files are throwing error:
Module build failed (from ../node_modules/postcss-loader/src/index.js):
Error: Cannot find module 'react-css-modules'
webpack.config.js:
{
test: /.css$/,
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]',
},
sourceMap: true,
importLoaders: 1,
},
},
{
loader: 'postcss-loader',
},
],
},
postcss.config.js:
plugins: [
require('postcss-inline-svg'),
require('postcss-import'),
require('postcss-pxtorem')({...}),
require('postcss-mixins')({...}),
require('react-css-modules'),
require('postcss-color-gray'),
require('postcss-preset-env')({
browserslist: [...],
stage: 3,
features: {
'custom-properties': {
preserve: false,
},
'nesting-rules': true,
'color-mod-function': { unresolved: 'warn' },
},
}),
require('postcss-extend'),
],
.bablerc:
"plugins": ["@babel/plugin-proposal-object-rest-spread", "lodash",
["react-css-modules", {
"webpackHotModuleReloading": true,
"exclude": "node_modules",
"generateScopedName": "[name]__[local]--[hash:base64:5]"
}
]
],
Any idea what's going wrong here?
Looks like you've got 'react-css-modules' as a plugin in your PostCSS config? It doesn't belong there, its just a Babel plugin.
Thanks!! It worked.
BTW, CSS Modules is just a bunch of PostCSS plugins inside
👍 Didn't know that! Perhaps I should retitle post: "Other PostCSS plugins with CSS Modules and React"?
The article name is great ☺. It is just a curious fact.
BTW, css-loader is PostCSS plugins too 😄.