There isn’t one single approach with CSS Modules to making the JavaScript templates, the CSS files, or the build steps to make them work. In this post, which is part of a series on CSS Modules, we’ll look at one approach. The goal of this post is to get a CSS Modules project up and running.
Article Series:
- What are CSS Modules and why do we need them?
- Getting Started with CSS Modules (You are here!)
- React + CSS Modules = 😍
In the projects I work on, there is a requirement that CSS should never rely on client-side JavaScript to work, so the build step needs to process everything into working HTML and CSS before it is deployed. We’ll be using webpack, a build system and module bundler. In the next post, we’ll focus on making the code below suitable for a real-life project that renders static HTML to the browser.
Let’s begin!
Installing webpack
After installing NPM and node we need to setup an empty directory somewhere and run the following:
npm init --y
This will make a package.json
file and fill it with a bunch of defaults. This is our dependency manifest – the instructions for what is downloaded and installed when other people npm install
this project.
webpack will be handling our build process. It will be watching our CSS, JavaScript, and HTML and performing all the magic in between. But what is webpack? Maxime Fabre wondered if webpack is a build system or a module bundler:
Well, it’s both—and by this I don’t mean that it does both I mean that it combines both. webpack doesn’t build your assets, and then separately bundle your modules, it considers your assets to be modules themselves…that can be imported, modified, manipulated, and that ultimately can be packed into your final bundle.
If this sounds weird, don’t worry. Remember when Sass and Gulp and npm were all unfamiliar and scary? We’ll figure it out.
Let’s makes sure webpack is “bundling” modules correctly by making one JavaScript file define a dependency so that we can import that chunk of code. First, we need to globally install webpack, which will give us access to the webpack
command in our terminals:
npm install webpack -g
Once that’s finished we need to install webpack locally in our project, like so:
npm i -D webpack
Now we need to make an index.js
file in a /src
directory. Typically I like to make a directory where all of the static assets reside (such as images, fonts, CSS files and markup). Any code that I write will typically live in a /src
directory, whilst any code that is written by a machine or interpreted in a certain process should live in a /build
directory. My thinking is that it ought to be totally OK to delete a /build
directory and not suffer any problems whatsoever because we can just run a command and it will process the stuff from /src
directory and entirely rebuild the /build
directory. In this case, we want webpack to take a look at everything in /src
, perform a certain process, and then move that code into /build
.
In the /src
directory we can also add an empty alert.js
file (we’ll return to it in a minute). We’ll also need a webpack.config.js
file that sits at the root of our project, outside the /src
directory so that our project structure should now look like this:
package.json
webpack.config.js
/node_modules
/src
index.js
alert.js
Inside webpack.config.js
(a file for configuring webpack, we can add the following:
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
};
Whenever we run the webpack
command from here on out, webpack will look at all of the assets in /src
to build a dependency tree.
Returning to our src/index.js
file we can add this:
require("./alert.js");
And inside our alert.js
file we can write this:
alert("LOUD NOISES");
Now let’s make an index.html
file in our root and add our bundle in a script tag just before the closes:
CSS Modules demo
<script src="build/bundle.js"></script>
That bundle.js
will be generated by webpack. To generate it all we have to do is run the webpack
command. To make this easier for ourselves, we can update our package.json
file with a build script. This is what you should find in that file:
"scripts": {
"test": "echo 'Error: no test specified' && exit 1"
},
Those are the defaults that npm
gave us, but we can replace the above with the following code to make our own command line script that will run webpack for us and open up a browser window:
"scripts": {
"start": "webpack && open index.html"
},
So whenever we run npm start
we’ll automatically run the webpack
command and open up our index file in the browser. Let’s do that now and see what happens.

Hurray, something is working! This proves that our index.js
file is importing our code from alert.js
and that webpack is bundling everything properly. If we now delete the alert.js
file we’ll find an error when we run npm start
again:

That’s the error that webpack will reveal if it can’t find an imported module. But now that we’ve confirmed that all of this works we can scrap that require
statement in our index.js
file and move onto the next step in learning about Webpack.
Adding Our First Loader
A loader in webpack is really important. Maxime Fabre has this to say on the subject:
Loaders are small plugins that basically say “When you encounter this kind of file, do this with it”.
In Maxime’s tutorial he adds the Babel loader, which is a really good starting point because Babel allows us to use ES2015 and the latest improvements to the JavaScript language. So instead of the Common.js function that we used earlier to require
another module we can use import
instead. With Babel we can also use classes, arrow functions and a bevy of other cool features:
Tools like Babel allow us to write new ES2015 code today and perform a task called transpiling (much like preprocessing) to convert the code into a earlier version of JavaScript that has greater browser support. This is similar to how Sass works; initially writing your code in Sass syntax, and then a preprocessor compiles to standard CSS.
The following will install the webpack Babel loader and the dependencies we need to run Babel:
npm i -D babel-loader babel-core babel-preset-env
In a .babelrc
file in the root of our project we can configure the preset to let others know which JavaScript syntax we’ll be using:
{
"presets": ["babel-preset-env"]
}
Now we want to run Babel on all of our .js
files but only the files that we write, any other dependencies that we install later might have their own syntax and we don’t want to mess with that code. This is where the webpack loader comes into play. We can open up webpack.config.js
file and replace that code with this:
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel-loader',
include: __dirname + '/src',
}
],
}
};
That test
key/value pair inside the loaders
array is how we tell webpack which type of file we want to perform an action on whilst include
tells it precisely where in our project we want that action to be performed.
Let’s test that Babel is working in conjunction with webpack. In a new file (src/robot.js
), let’s write the following:
const greetings = (text, person) => {
return `${text}, ${person}. I read you but I’m sorry, I’m afraid I can’t do that.`;
}
export default greetings;
This JavaScript file is using a bunch of ES2015 specific features, like export
, const
and let
, arrow functions, and template literals.
Now we can import
that module into our src/index.js
file, like so:
import greetings from './robot.js'
document.write(greetings("Affirmative", "Dave"));
Finally, all we need to do is run npm start
again and our browser should pop back with the text: “Affirmative, Dave. I read you but I’m sorry, I’m afraid I can’t do that.” This simply confirms that Babel is working as it should.
Hurray! That’s not a CSS Module yet, although we’re certainly one step closer. But before we move on let’s delete src/robot.js
and all the code from src/index.js
.
Loading the styles
Now that we’ve got our templates almost working we’ll need to add two more loaders: css-loader and style-loader, which we’ll install:
npm i -D css-loader style-loader
The css-loader takes a CSS file and reads off all its dependencies whilst the style-loader will embed those styles directly into the markup. Let’s test this by writing some CSS in src/app.css
:
.element {
background-color: blue;
color: white;
font-size: 20px;
padding: 20px;
}
Then we can import
that stylesheet into our src/index.js
file:
import styles from './app.css'
let element = `
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur laudantium recusandae itaque libero velit minus ex reiciendis veniam. Eligendi modi sint delectus beatae nemo provident ratione maiores, voluptatibus a tempore!
`
document.write(element);
Whoa, hang on! Did we just make a stylesheet a dependency of a JavaScript file? Hell yes we did. But before it works properly, and before we see why this is useful, we first need to reconfigure our webpack.config.js
again:
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.css/,
loaders: ['style', 'css'],
include: __dirname + '/src'
}
],
}
};
Running npm start
will leave us with something like this:

Consequently, if we “Inspect Element” on our document we’ll find that the style-loader has placed that file into a
<style>
tag in the <head>
of the document:

Let’s take stock of what just happened. We made a JavaScript file that requested another CSS file and that code was then embedded within a web page. So in a more realistic example we could create a buttons.js
file and make buttons.css
a dependency of it, and then import that JavaScript into another file that organises our templates and spits out some HTML. This ought to make a our code absurdly modular and easy to read!
Personally, just to keep things clean, I’d prefer to have a separate CSS file rather than adding all the code inline. To do that we’ll need to use a webpack plugin called extract text which:
moves every require(‘style.css’) in entry chunks into a separate css output file. So your styles are no longer inlined into the javascript, but separate in a css bundle file (styles.css). If your total stylesheet volume is big, it will be faster because the stylesheet bundle is loaded in parallel to the javascript bundle.
We have to install that with npm:
npm i -D extract-text-webpack-plugin
Now we can update our webpack.config.js
file again by requiring it and placing our CSS loader into it:
var ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: './src',
output: {
path: 'build',
filename: 'bundle.js',
},
module: {
loaders: [
{
test: /\.js/,
loader: 'babel',
include: __dirname + '/src',
},
{
test: /\.css/,
loader: ExtractTextPlugin.extract("css")
}
],
},
plugins: [
new ExtractTextPlugin("styles.css")
]
};
ExtractTextPlugin
will now create a styles.css
file for us!
You might’ve noticed that we’ve gotten rid of style-loader entirely. That’s because we don’t want those styles injected into our markup any more. So now if we open up the /build
directory, we should find that a styles.css
file has been created with all of our code inside. And within our index.html
file, we can now add our stylesheet in the <head>
:
<link rel="stylesheet" href="build/styles.css">
Run npm start
again and blammo! – our styles magically appear back on the page where they belong.
Now that we have our CSS and HTML working on the page, how do we manipulate the class names in order to get all the benefits of a local scope? All we have to do is update our webpack.config.js
file like so:
{
test: /\.css/,
loader: ExtractTextPlugin.extract('css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'),
}
This will add the crazy generated text to the end of the class name. That’s all that CSS Modules really is, a hash which changes the classes which can be added in webpack via a CSS loader.
Next, we have to update our index.js
file with the styles.element
class:
import styles from './app.css'
let element = `
<div class="${styles.element}">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequatur laudantium recusandae itaque libero velit minus ex reiciendis veniam. Eligendi modi sint delectus beatae nemo provident ratione maiores, voluptatibus a tempore!
</div>
`
document.write(element);
Look what happens! One more npm start
and our code has now been processed by webpack so local scope is no longer an issue, since the class that gets injected into the web page now looks like this:
<div class="app__element___1MmQg">
...
</div>
We’re still not really finished as there are many questions left unanswered. How could we write code like this in development? How do we get around that nasty document.write
rule we’re using to inject the markup into the page? How should we structure our modules and files? Getting CSS Modules up and running is only half the work, next we have to think about how we might port a code base into it from another system.
In the next tutorial we’ll be taking a look at how React can help us generate tidy little modules, also we’ll see how we can generate static markup from a number of templates and how to add other features such as Sass and PostCSS to our project.
Article Series:
- What are CSS Modules and why do we need them?
- Getting Started with CSS Modules (You are here!)
- React + CSS Modules = 😍
Heh— boy do I ever feel like that sometimes.
In real-world environments how many of these ‘modules’ do you tend to see on a page? Do those appended hashes adversely affect gzipping much?
Thanks so much for writing this! This is really well-timed; I had planned to start a new project today using CSS Modules, so waking up and seeing this article posted was perfect.
I’m encountering an error with webpack though. I’ve done everything up to “Loading the styles” so I have a the
index.js
androbot.js
files inside my src directory, and even copy-pasted both of those and webpack.config.js from your examples directly to ensure I wasn’t messing something up there, but webpack is still giving me this error:It seems it doesn’t like the
import
syntax. I’ve done a lot of searching around and have tried various fixes I’ve seen either on S/O or on GitHub issues, but nothing seems to be doing it for me :( After trying a few different edits to the webpack.config.js file that didn’t end up working, I thought maybe somehow my package.json was messed up (as has been the case with others who have had this error), but I passed that through an online validator and didn’t see any issues. Here’s the full repository, if anyone feels so inclined to have a peek.Is anyone else running into this issue? :(
Hours later, finally got it fixed. For some reason, in the
include
fields in the loaders, it didn’t like me using__dirname + '/src'
, so instead I tried puttingvar path = require('path');
at the top of my webpack config file, and in theinclude
fields, I didpath.resolve('src')
instead of trying to use__dirname + '/src'
. No idea why this happens, but I am relieved all the same :DHuh. I ran through the demo again and couldn’t replicate that bug. Sorry about all the hassle!
No worries :) Seems like it wasn’t really specific to this tutorial anyway; I suspect there must have been something else going on on my end that I messed up, or some other oddity.
Thanks again for this wonderful writeup :D
I also had this problem, it took me hours. Thanks for the solution.
Thanks for this Robin, local scoping CSS is something I’ve wanted to do for a while. It’s just a shame there is no native way of (easily) doing it and that we’re forced to adopt new processes. That said, I’ve enjoyed following your tutorial and I’m looking forward to part 3!
I do think at a high level global scope is useful though – to ensure a
.button
class always has the same brand colour applied for instance. Would it make sense to reference a staticglobal.css
file as well as a generatedscoped.css
file in the header with this approach? A hybrid approach if you will. Then I could write<a class="button ${styles.big}">
and know that the “big” augmentation will always be unique to that element/module, but all buttons will be the same colour globally. Does that make sense?You certainly could. Although I think that having two CSS files would be quite confusing and ideally if we’re implementing a solution to limit the cascade then we should use it everywhere at all times.
There’s nothing stopping us here from importing a
.button
class into the template and then using<a class="{button} {button.big}">
and keeping all those styles insidebutton.css
. I would certainly worry about the alternative, of having two CSS files, simply because it’ll be difficult to maintain over time.Great article with detailed explanation. Thank you!
Thanx for the article, man! Nice job, can’t wait to read the next one.
I have a question regarding this topic. I wonder what is the benefit and difference when using styles as Blob links over standard ‘style’ tag in head. Because sometimes Webpack generates something like:
I’ve asked it here too: http://stackoverflow.com/questions/36678077/blob-based-link-stylesheet-vs-standard-style-tag
anyone? ;)
Awesome article, but some of your npm commands, while valid, are inconsistent. For example, switching from
npm install
tonpm i
. I didn’t know that was an alias. Okay, so some might argue that’s self-explanatory, but you also threw innpm i -D
and that sent me to Google for a little while. I still don’t know what the-D
flag is. I’ll be pretty bummed I spent so much time searching for it if it is an alias for--save
or--save-dev
.How to dynamically require css depend on different routes in this way?
where is part3?
1
ermm… that should have been a “plus 1”. I think markdown ate the plus symbol ;)
Robin, I can’t begin to tell you how awesome this write-up is. I love the simplicity of your explanation of every step, even down to (seemingly) trivial details. I’ve been trying to suss out webpack for a while and this is the first time where the word ‘loader’ suddenly makes total sense. I wish you’d write up a whole series on Webpack! (what is that module.exports = merge(common, …) ?? and TARGET and PATHS?? I wish I could know!)
Looking forward to the next article!
When is the Part 3 coming? I’m waiting desperately for that.
will you cover configuring hot module replacement in the next installment?
In the projects I work on, there is a requirement that CSS should never rely on client-side JavaScript to work, so the build step needs to process everything into working HTML and CSS before it is deployed.
Could you elaborate more, for a newbie, on why using a preprocessor successfully handles the necessity of avoiding ‘client-side Javascript’, a requirement I’m guessing is for security reasons? Surely there’s still scripts being run by the browser(client)? Thanks.
Hi Sarah — I’m a little confused by the question but I’ll try my best to answer. A preprocessor, like Sass or LESS, doesn’t require JavaScript by the browser because that code isn’t sent to them at all. Those files are a part of the “build step” so that a developer can write in Sass but that gets eventually gets transformed, or compiled, into CSS— so a file such as styles.css is what is actually sent to the client.
In short: preprocessors are for developer convenience only and don’t have much to do with JavaScript at all.
CSS Modules are very much the same: they don’t require JavaScript in the browser, although the method I talk about here does require JavaScript, that’s only because we’re using Webpack. If you want to read more about a more realistic example than Part 3 goes into a little more depth.
Did I answer your question? I’m more than happy to elaborate on CSS Modules or preprocessors if you’d like.