CSS Modules with Webpack 4

Ok, I know what you are going to say. We have heard of modules with Programming languages, but modules in CSS. Is that even possible? and why do you need it at first place? Well, I had a similar reaction when I heard about it. CSS Modules were born out of the idea of relentless hours spent in debugging issues caused by overlapping CSS selectors, class names. It is a common practice of having multiple style declarations spread across CSS files. The web projects include multiple CSS files to define common layout appearance, widgets etc. This sometimes becomes a nightmare when you have multiple CSS files sharing same class selector.

Dive into the Problem

Let’s have a look at the problem in detail so that you can easily relate to the situation. Consider following CSS files.

style.css

.my_css_selector {
color: blue;
}

#Other CSS styles

app.css

.my_css_selector {
color: green;
}

#Other CSS styles

And we have following HTML file, which is referencing both of these files.
index.html

<!doctype html>
<html>
<head>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="app.css">
</head>
<body>
<h2 class="my_css_selector">I should be displayed in blue.</h2>
<br/>
<h2 class="my_css_selector">I should be displayed in green.</h2>
</body>
</html>

How can you enforce that both the text elements should display their respective colors? Renaming CSS selectors may be an option, but it may not be feasible under every circumstance. What if, the CSS has been downloaded from Third Party? Making changes to the file will always lead to upgrade challenge. But luckily there is a better option – CSS Module.

CSS Module with Webpack

CSS Module with Webpack

What is CSS Modules?

CSS Modules is not officially a specification, but a concept that can be easily accomplished using Webpack and other plugins. In fact, there is GitHub repo which describes CSS Modules as

CSS Module is a CSS file in which all class names and animation names are scoped locally by default.

Let’s Get Practical

To understand it better let’s build a sample NodeJS webpage. To begin with, create a directory at a location of your choice and execute the following command.

npm init --y

Above command will create package.json file with default properties. Create index.html file and src folder at the same location and your folder structure should look like below.

HTML project

HTML project

We will simulate above example in this project. Also, it is necessary that to use CSS modules the references must be made from JS file. Hence, we will define the HTML text in JS file.

The two style files should be created in src folder with following definitions.

style.css

.my_css_selector {
color: blue;
}

app.css

.my_css_selector {
color: green;
}

Create index.js file in src folder with following contents.

let html = `
<h2 class="my_css_selector">I should be displayed in blue.</h2>
<br/>
<h2 class="my_css_selector">I should be displayed in green.</h2> 
`;
document.write(html);

Time to create index.html, which should reference all the above files.

<!doctype html>
<html>
<head>
<link rel="stylesheet" href="src/style.css">
<link rel="stylesheet" href="src/app.css">
</head>
<body>
<script src="src/code.js">
</script>
</body>
</html>

Open the index.html file and you should see both the texts are displayed in green color like below.

Webpage before CSS Module

Webpage before CSS Module

To address the problem, we will use Webpack and its loader ecosystem and develop CSS Modules.

Install Dependencies

We will require Webpack to build the modules. If you are not familiar with Webpack, Webpack is a build tool and module bundler. As per the official website,

At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.

Install Webpack dependency using the following command

npm i -D [email protected] webpack-cli

Above command will install latest Webpack version as Development Dependency. Note that Webpack is a build tool and hence we don’t require it at the runtime. We will use the output file produced by the Webpack.

npm i -D css-loader style-loader

CSS Loader will read CSS files and its dependencies, while style loader will embed those styles in a markup.

Since we will be using ES2015 syntax, it is necessary to install babel dependency as well. This will ensure that browsers which don’t support ES2015 will still work with our code.

npm i -D babel-loader babel-core babel-preset-env

Time to create Webpack configuration file so that we can use it in our project. Create webpack.config.js at the root of the project and add following contents.

module.exports = {
	entry: './src',
	output: {
		path: __dirname + '/build',
		filename: 'bundle.js'
	},
	module: {
		rules: [
			{
				test: /\.js/,
				loader: 'babel-loader',
				include: __dirname + '/src'
			},
			{
				test: /\.css/,
                loaders: ["style-loader", "css-loader"],
                include: __dirname + '/src'
			}
		]
	}
}

Modify package.json to specify scripts section. Refer to below snippet for reference.

{
  "name": "blog-css-module",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "babel-core": "^6.26.3",
    "babel-loader": "^7.1.4",
    "babel-preset-env": "^1.6.1",
    "css-loader": "^0.28.11",
    "style-loader": "^0.21.0",
    "webpack": "^4.0.0-beta.3",
    "webpack-cli": "^2.1.2"
  }
}

Now execute the following command to compile all the content.

npm start

Modify index.html file and replace it with the following content.

<!doctype html>
<html>
<head>
</head>
<body>
<script src="build/bundle.js">
</script>
</body>
</html>

We have removed references of styles and replaced index.js location to bundle.js. Webpack checks the dependencies and it inlines all the dependencies in the output file i.e. bundle.js in our case. Save the file and reload the browser. You will see that the browser now displays both the statements in black color.

Webpage webpack before css import

Webpage webpack before css import

Wait, What??? we lost the formatting and why our CSS definitions are not taking the effect. Webpack scans through the source code and only includes the dependencies that are explicitly referenced. In the modified index.js we haven’t yet made any references to styles. Change index.js file to following.

import bluestyle from './style.css';
import greenstyle from './app.css';

let html = `
<h2 class="${bluestyle.my_css_selector}">I should be displayed in blue.</h2>
 
<h2 class="${greenstyle.my_css_selector}">I should be displayed in green.</h2>
`; 
document.write(html);

Run npm start again and reload the browser. You should see following output.

Webpage webpack before css import

Webpage webpack before css import

Why is the page not taking effect? The reason is Webpack has embedded the CSS files inside bundle.js and at runtime, it will be available as inline style block in HTML. Inspect the webpage and you should see following.

<head>
<style type="text/css">.my_css_selector {
 color: blue;
 }</style><style type="text/css">.my_css_selector {
 color: green;
 }</style></head>

It will be better if we can externalize these CSS declarations in another file and include it in HTML. To do that install extract-text-webpack-plugin.

npm i -D [email protected]

Note that the current release 3.X.X has some issues with Webpack 4 and hence we are using the beta version of Extract Plugin. The GitHub repo defines the plugin as

Extract text from a bundle, or bundles, into a separate file.

Modify webpack.config.js file and replace it with following contents.

var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
	entry: './src',
	output: {
		path: __dirname + '/build',
		filename: 'bundle.js'
	},
	module: {
		rules: [
			{
				test: /\.js/,
				loader: 'babel-loader',
				include: __dirname + '/src'
			},
			{
				test: /\.css/,
				loader: ExtractTextPlugin.extract("css-loader?modules&importLoaders=1&localIdentName=[name]__[local]__[hash:base64:5]")
			}
		]
	},
	plugins: [
		new ExtractTextPlugin("styles.css")
	]
}

Note the CSS section has been replaced with ExtractTextPlugin and the output file name has been configured in plugins section. Modify index.html file to include new style file.

<!doctype html>
<html>
<head>
 <link rel="stylesheet" href="./build/styles.css" >
</head>
<body>
<script src="build/bundle.js">
</script>
</body>
</html>

We are at the final stage now, just run npm run again at the command prompt and reload the browser. You should see following output.

CSS Module with Webpack

CSS Module with Webpack

If you inspect the elements, you will find following HTML

<h2 class="style__my_css_selector__3aCWo">I should be displayed in blue.</h2>

The name of the CSS class has changed to filename_cssSelector_randomHash. You can customize this format by modifying loader property defined in webpack.config.js file for CSS loader.

I hope you may have now realized by using CSS Modules you can modularize your CSS definitions and also avoid countless hours solving nasty CSS errors.

Source Code for this post can be found at Github.

Be Sociable, Share!

Leave a Comment.