loading...

PostCSS with CSS Modules and React

daveirvine profile image Dave Irvine Updated on ・4 min read

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.

Posted on by:

Discussion

pic
Editor guide
 

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 classes btn btn-primary it's resulting in src-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 leaving className.

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. :)

 
 

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.

 

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.

 

I was looking for a way to have scoped styles that I could use by writing import styles from './my.css' and using them like className={styles.someClass}.

Without setting up anything further than was given here I made it. Any reason why you chose to use styleName? In my case, I'd rather stick to a well established attribute name. On the other hand, I can import more than one module to my components and use other styles. This is useful for when I have global styles that might apply in many places.

 

So styleName comes from the babel-plugin-react-css-modules plugin. Stolen from their docs:

However, there are several several disadvantages of using CSS modules this way:

  • You have to use camelCase CSS class names.
  • You have to use styles object whenever constructing a className.
  • Mixing CSS Modules and global CSS classes is cumbersome.
  • Reference to an undefined CSS Module resolves to undefined without a warning.

Using babel-plugin-react-css-modules:

  • You are not forced to use the camelCase naming convention.
  • You do not need to refer to the styles object every time you use a CSS Module.
  • There is clear distinction between global CSS and CSS modules, e.g.
<div className='global-css' styleName='local-module'></div>
 

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 😄.