Getting Started with CSS Modules
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
Part 1: What are CSS Modules and why do we need them?
Part 2: How to get started with CSS Modules (You are here!)
Part 3: React + CSS Modules = ? (Coming soon!)
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:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document name</title>
</head>
<body>
<h1>CSS Modules demo</h1>
<script src="build/bundle.js"></script>
</body>
</html>
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-es2015
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": ["es2015"]
}
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',
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 = `
<div class="element">
<p>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!</p>
</div>
`
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 tag in the
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 :
<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}">
<p>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!</p>
</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
Part 1: What are CSS Modules and why do we need them?
Part 2: How to get started with CSS Modules (You are here!)
Part 3: React + CSS Modules = ? (Coming soon!)
Getting Started with CSS Modules is a post from CSS-Tricks