Today we're going to talk about CSS again. The topic is CSS Modules, which is very interesting and very simple to understand.
What is it?
CSS Modules is some sort of specification for CSS. Like the name implied, this specification is mainly made to introduce the concept of modules in CSS. Beside modules, CSS Modules also specified other stuff, such as class naming convention, and composing styles.
Why use it?
Before, CSS's class names are always globally defined. As your project gets bigger, this global naming could be a problem. For example, when you defined the styling of a class named .square twice in two different files. The result styling could be hard to notice and debug. CSS Modules offered a solution by scoping the definition of CSS classnames.
How to use it?
CSS Modules specification is implemented in Webpack and PostCSS.
Since we talked about Webpack last week, I'm going to use Webpack's implementation. In Webpack, CSS Modules is implemented in css-loader. If you read my post about Webpack, you might be familiar with it.
Here are the steps:
- Install
webpackandwebpack-cliin your project
npm install webpack webpack-cli --save-dev - Install
style-loaderandcss-loaderin your project.
npm install style-loader css-loader --save-dev - Create webpack.config.js with the following content:
Notice theconst path = require('path'); module.exports = { entry: './src/main.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.css$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader', options: { modules: true, localIdentName: '[path][name]__[local]--[hash:base64:5]' } } ] } ] } };css-loader'soptionsproperty. We setmodulestotrueto enable CSS Modules in Webpack. I'll explainlocalIdentNameproperty a little bit later. - Now, we're going to demonstrate the power of CSS Modules by building a simple web page.
This web page will display a list of cards that contain a pet's name and description. Here's the final look of the web page:
- We will encapsulate the card as a component. Below here is the project structure. First I'll show you the content of the CSS files (src/main.css and src/card/main.css) and then the JS files.
. ├── dist │ ├── index.html # the main page ├── src │ ├── main.js # main page's script │ ├── main.css # main page's style │ ├── card │ | ├── main.js # card's script │ | ├── main.css # card's style ├── package.json ├── webpack.config.jsdist/index.html
<!doctype html> <html> <head> <title>CSS Modules</title> </head> <body> <div id="app"></div> <script src="./bundle.js"></script> </body> </html>src/main.css
This is the main page's styling. We define three classes:.container,.title, and.description..container { padding: 10px; border: 1px solid blue; } .title { font-size: 15px; } .description { font-size: 12px; }src/card/main.css
This is card's styling. Notice that in this file, we define the exact same classes as in the main's page styling. Without CSS Modules (where all is globally defined), this condition is not ideal and the styling would be a mess.
Also notice that we use the:globalkeyword for.card-shadow. This is a special keyword introduced by CSS Modules. I'll explain this later..container { padding: 5px; border: 1px solid red; } .title { font-size: 10px; } .description { font-size: 8px; } :global(.card-shadow) { box-shadow: 1px 1px 2px 1px grey; }src/button/main.js
Now that the CSS files are setup. We'll look into the JS files. Starting from the card's JS file.
As you can see below, first, we have to import the card's styling as an object (style). Then, we use the object whenever we wat to add a class to an element.
For example, here we add the.containerclass to variablerootby accessingstyle.container.
You can access all defined classname by this syntax:style.{className}.// import style as an object import style from './main.css'; export default function Card(title, description) { var root = document.createElement('div'); // use the imported object by accessing its property root.classList.add(style.container, 'card-shadow'); var t = document.createElement('h3'); t.classList.add(style.title); // use it here again t.innerHTML = title; var d = document.createElement('p'); d.classList.add(style.description); // use it here again d.innerHTML = description; root.appendChild(t); root.appendChild(d); return root; }src/main.js
We do the same thing on the main page's script here.import style from './main.css'; import Card from './card/main.js'; function app() { var root = document.createElement('div'); root.classList.add(style.container, 'card-shadow'); var t = document.createElement('h3'); t.classList.add(style.title); t.innerHTML = 'My Pet List'; var d = document.createElement('p'); d.classList.add(style.description); d.innerHTML = 'These are my pets:'; root.appendChild(t); root.appendChild(d); var c = Card('Cat', 'Useless pet'); var d = Card('Dog', 'Useful pet'); root.appendChild(c); root.appendChild(d); return root; } document.getElementById('app').appendChild(app()); - And that's it! If you open index.html, you'll get the same result as I showed earlier.
Now, there are two explanations that I postponed, which are localIdentName property and :global keyword.
localIdentNameproperty is used to specify the renaming of CSS classnames. Under the hood,css-loaderimplemented CSS Modules by renaming all classnames, so that they are scoped to their modules. For example, we defined thelocalIdentNameproperty in webpack.config.js as this:
This configuration would rename theoptions: { localIdentName: '[path][name]__[local]--[hash:base64:5]' }.containerclass in src/main.css as this:
And thesrc-main__container--A7AOC.containerclass in src/card/main.css would be renamed to this:
With this mechanism, we can use the same classname for different modules. Really cool!src-card-main__container--1f2By:globalkeyword is used as an exception to the CSS Modules. The classnames that are preceded by this keyword would not be renamed bycss-loader. For example, we use:globalfor the.card-shadowclassname in the card's styling. This classname would not be renamed and can be used globally by the main page or any other module.
That's it for now!