Understanding the CSS Modules Methodology
In the ever-changing world of front-end development, it is quite hard to find a concept that actually makes a difference, and to introduce it correctly enough so that other people are actually willing to try it.
When we look at CSS, I would say the last big shift that happened in the way we write CSS (on the tooling side) would have to be the arrival of CSS processors, mostly Sass, probably the most well-known one. There’s also PostCSS, which has a slightly different approach but ends up essentially in the same basket – browser-unsupported syntax goes in, browser-supported syntax goes out.
Now a new kid has joined the block under the name CSS Modules. In this article, I’d like to introduce this technique, tell you why it has some good points, and how you can get started with it.
What is a CSS Module?
Let’s start with the explanation from the official repository:
A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.
Now, it is a little more complex than that. Because class names are scoped locally by default, it involves some setup, a build process, and a bit of magic that is sometimes hard to grasp.
But eventually, we can appreciate CSS Modules for what they are: a way to scope CSS to a component and escape the global namespace hell. No more struggling to find a good way to name your components, because the build step is doing that for you!
How does it work?
CSS Modules need to be piped in a build step, which means they do not work by themselves. See this as a plugin for either webpack or Browserify. It basically works this way: when you import a CSS file inside a JavaScript module (such as, but not necessarily, a React component), CSS Modules will define an object mapping class names from the file to dynamically scoped/namespaced class names that can be used as strings in JavaScript. Allow me to illustrate with an example:
Below is a very simple CSS file. The .base
class does not have to be unique on the project as it is not the actual class name that will be rendered. It is just a kind of alias inside the stylesheet to be used in the JavaScript module.
.base {
color: deeppink;
max-width: 42em;
margin: 0 auto;
}
And here is how you would use it in a dummy JavaScript component:
import styles from './styles.css';
element.innerHTML = `<div class="${styles.base}">
CSS Modules are fun.
</div>`;
Eventually, this will generate something like this (when using it through webpack with the default setup):
<div class="_20WEds96_Ee1ra54-24ePy">CSS Modules are fun.</div>
._20WEds96_Ee1ra54-24ePy {
color: deeppink;
max-width: 42em;
margin: 0 auto;
}
The way class names are generated can be configured to make them shorter, or to follow a specific pattern. It doesn’t really matter in the end (although shorter class names mean shorter stylesheets) because the point is that they are dynamically generated, unique, and mapped to the correct styles.
Possibly valid concerns
So this is how it works. And now, you must be thinking “what on earth is this thing? I don’t even…”. Well, hold on! I hear you. Let’s tackle those concerns one by one, shall we?
It looks friggin’ ugly!
That is true. But class names are not intended to be beautiful; they are intended to apply styles to elements. And this is precisely what they do, so I would argue this is not a valid concern.
It is a pain to debug!
As soon as there is a build step to do some processing on your stylesheet, it is a pain to debug. Sass is not any easier. This is why we have sourcemaps, which you can set up for CSS Modules as well.
I would actually argue that despite the unpredictability of class names, it is not that hard to debug as styles are, by definition, specific to modules. If you know what module you are inspecting in the dev tools, you know where to look for the attached styles.
It makes styles non-reusable!
Yes and no. On the one hand, yes, but this is actually the point: it ties styles to a component to avoid global styles leaking and conflicts. Which is a good thing, I’m sure you will concede.
On the other hand, it is still possible to define global classes (with :global()
) such as helpers which are preserved as authored, making style abstracting as easy as usual. These classes can then be used in your JavaScript components.
:global(.clearfix::after) {
content: '';
clear: both;
display: table;
}
CSS Modules also have a way to extend styles from another module, which basically works the same way as @extend
in Sass. It does not copy the styles but concatenates selectors to extend styles.
.base {
composes: appearance from '../AnoherModule/styles.css';
}
It needs webpack, Browserify or [whatever] tool!
This is the same way Sass needs to compile the .scss
files to actual CSS, and PostCSS needs to process the stylesheets to make them compatible with browser-understood styles. The build step is already there anyway.
Why are we even discussing this?
Well, I am not entirely sure that in the future CSS Modules will stay the way they are currently, but I do think this is a sensible way to write styles. Having a massive global stylesheet is not suitable for large sites decoupled in tiny components.
The unique namespace of CSS makes it both powerful and very fragile at the same time. Such a solution, be it CSS Modules or any future tool based on this idea, allows us to preserve the strength of style-sharing with global classes while making it a breeze to avoid naming conflicts with scoped styles. Win-win.
Getting started
As said above, you will need either webpack or Browserify to enable CSS Modules.
Webpack
Let’s start with the webpack version. In the webpack.config.js
, add the following configuration to tell webpack to treat CSS files with CSS Modules:
{
test: /\.css$/,
loader: 'style-loader!css-loader?modules'
}
Right now it will inject styles inside a <style>
element in your page. This is probably not what you want, so let’s make a proper stylesheet out of it thanks to the extract text plugin for webpack:
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader?modules')
}
That’s pretty much it for webpack.
Browserify
I’ve only ever used Browserify through the command line, so it is a bit trickier I guess. What I did was add an npm script to my package.json
file, like so:
{
"scripts": {
"build": "browserify -p [ css-modulesify -o dist/main.css ] -o dist/index.js src/index.js"
}
}
This tells Browserify to run on src/index.js
, output dist/index.js
, and compile a stylesheet at dist/main.css
through the css-modulesify plugin. If you want to add Autoprefixer to it, you can complete the command like so:
{
"scripts": {
"build": "browserify -p [ css-modulesify --after autoprefixer -o dist/main.css ] -o dist/index.js src/index.js"
}
}
As you can see, you can use the --after
option to process your stylesheet after having it compiled.
Conclusion
As of today, the CSS Modules system and ecosystem is a bit raw, as you can see from the Browserify configuration. But I am convinced it is going to get better as more people realize this is a sustainable solution for small to large projects.
I think the idea behind CSS Modules is the way to go. I am not saying this library in particular is the best solution available, but it definitely has some features with regard to the way CSS should be written: modular, scoped, yet still reusable.
As a further read, I recommend this introduction to CSS Modules by Glen Maddern, creator of the project.