Getting Started with CSS Modules

Share this:

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:

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

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.

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:

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:

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:

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:

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:

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:

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:

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.

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 and robot.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.

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 putting var path = require('path'); at the top of my webpack config file, and in the include fields, I did path.resolve('src') instead of trying to use __dirname + '/src'. No idea why this happens, but I am relieved all the same :D

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 static global.css file as well as a generated scoped.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 inside button.css. I would certainly worry about the alternative, of having two CSS files, simply because it’ll be difficult to maintain over time.

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:

Awesome article, but some of your npm commands, while valid, are inconsistent. For example, switching from npm install to npm i. I didn’t know that was an alias. Okay, so some might argue that’s self-explanatory, but you also threw in npm 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.

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!)

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.

This comment thread is closed. If you have important information to share, please contact us.

Related

How do you stay up to date in this fast⁠-⁠moving industry?

A good start is to sign up for our weekly hand-written newsletter. We bring you the best articles and ideas from around the web, and what we think about them.

👋

CSS-Tricks* is created, written by, and maintained by Chris Coyier and a team of swell people. It is built on WordPress and powered up by Jetpack. It is made possible through sponsorships from products and services we like.