Key Takeaways
- CSS Modules provide a method to scope CSS to a component, avoiding global namespace issues and making naming components easier. They require setup, a build process, and are not standalone, needing to be piped in a build step, usually as a plugin for webpack or Browserify.
- CSS Modules work by importing a CSS file inside a JavaScript module, defining an object mapping class names from the file to dynamically scoped/namespaced class names that can be used as strings in JavaScript. Class names are dynamically generated, unique, and mapped to the correct styles, allowing for a reduction in naming conflicts and a more modular approach to writing CSS.
- Despite some concerns about the aesthetics of the class names, the difficulty of debugging, and the potential for styles to be non-reusable, CSS Modules offer a way to avoid global styles leaking and conflicts. They also allow for the definition of global classes and the extension of styles from another module, promoting reusability and maintainability.
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.
Frequently Asked Questions about CSS Modules
What are the benefits of using CSS Modules?
CSS Modules offer a number of benefits that can enhance your web development process. Firstly, they help to eliminate the risk of class name collisions, which can be a common issue in large projects. This is achieved by locally scoping CSS by default. Secondly, CSS Modules allow for the creation of reusable components, which can significantly improve code efficiency and maintainability. Lastly, they support composition, which means you can extend one class with another, thereby promoting DRY (Don’t Repeat Yourself) principles in your code.
How do I implement CSS Modules in my project?
To implement CSS Modules, you need to configure your project’s build system to understand them. This typically involves using a tool like Webpack or Browserify. In your CSS file, you define your styles as you normally would. However, when you import the CSS file into a JavaScript module, it exports an object with all class names. You can then use these class names in your component. Remember to name your CSS files with the .module.css
extension to tell Webpack to treat them as CSS Modules.
Can I use global styles with CSS Modules?
Yes, CSS Modules support global styles. You can define global styles using the :global
scope. This is useful when you want to apply styles across multiple components or override locally scoped styles.
How does CSS Modules handle composition?
CSS Modules handle composition using the composes
keyword. This allows you to extend one class with another, effectively inheriting the styles. It’s a powerful feature that promotes code reuse and maintainability.
Can I use CSS Modules with React?
Yes, CSS Modules are fully compatible with React and other JavaScript frameworks. In fact, they are a popular choice for styling React components due to their support for component-based architecture.
How does CSS Modules differ from traditional CSS?
The main difference between CSS Modules and traditional CSS is the way they handle scope. In traditional CSS, all styles are global, which can lead to class name collisions. CSS Modules, on the other hand, scope styles locally by default, eliminating this issue.
Can I use CSS Modules with Sass or Less?
Yes, CSS Modules can be used with pre-processors like Sass or Less. You just need to configure your build system to process the pre-processor files before applying CSS Modules.
How do I debug CSS Modules?
Debugging CSS Modules can be a bit tricky due to the generated class names. However, you can configure your build system to include the original class name in the generated one, which can make debugging easier.
Are there any limitations to using CSS Modules?
While CSS Modules offer many benefits, they do have some limitations. For instance, they don’t support media queries or keyframes out of the box. However, these features can be added with additional configuration.
Can I use CSS Modules with server-side rendering?
Yes, CSS Modules can be used with server-side rendering. However, it requires additional configuration to ensure the correct styles are included in the server-rendered HTML.
Non-binary trans accessibility & diversity advocate, frontend developer, author. Real life cat. She/her.