DEV Community

Dave Irvine
Dave Irvine

Posted on • Edited on

PostCSS with CSS Modules and React

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
Enter fullscreen mode Exit fullscreen mode

Make sure your .babelrc or whatever equivalent you are using to configure Babel includes the new plugin:

plugins: [‘react-css-modules’]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]',
        },
      },
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

App.jsx

import React from 'react';

import './App.css';

const App = () => (
  <div styleName="app">
    Hello, world!
  </div>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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' }
  ],
}
Enter fullscreen mode Exit fullscreen mode

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,
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

Now we can try it out! Make a new colors.css file

:root {
  --errorRed: #e03030;
}
Enter fullscreen mode Exit fullscreen mode

and change App.css to use it:

@import "../colors.css";

.app {
  border: 1px solid var(--errorRed);
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
roblevintennis profile image
Rob Levin

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!

Collapse
 
daveirvine profile image
Dave Irvine

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

Collapse
 
roblevintennis profile image
Rob Levin

Gotcha thanks

Collapse
 
niksajanjic profile image
Nicky Jay

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

Collapse
 
daveirvine profile image
Dave Irvine

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?

Collapse
 
niksajanjic profile image
Nicky Jay

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.

Collapse
 
daveirvine profile image
Dave Irvine • Edited

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>
Collapse
 
gsaran profile image
gsaran

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?

Collapse
 
daveirvine profile image
Dave Irvine

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.

Collapse
 
gsaran profile image
gsaran

Thanks!! It worked.

Collapse
 
iskin profile image
Andrey Sitnik

BTW, CSS Modules is just a bunch of PostCSS plugins inside

Collapse
 
daveirvine profile image
Dave Irvine

👍 Didn't know that! Perhaps I should retitle post: "Other PostCSS plugins with CSS Modules and React"?

Collapse
 
iskin profile image
Andrey Sitnik

The article name is great ☺. It is just a curious fact.

BTW, css-loader is PostCSS plugins too 😄.