Getting Started with CSS Modules

Avatar of Robin Rendle
Robin Rendle on (Updated on )

📣 Freelancers, Developers, and Part-Time Agency Owners: Kickstart Your Own Digital Agency with UACADEMY Launch by UGURUS 📣

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:

  1. What are CSS Modules and why do we need them?
  2. Getting Started with CSS Modules (You are here!)
  3. 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' &amp;&amp; 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 &amp;&amp; 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) =&gt; {
  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:

  1. What are CSS Modules and why do we need them?
  2. Getting Started with CSS Modules (You are here!)
  3. React + CSS Modules = 😍