All Posts

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:

  1. Install webpack and webpack-cli in your project
    npm install webpack webpack-cli --save-dev ​
  2. Install style-loader and css-loader in your project.
    npm install style-loader css-loader --save-dev
  3. Create webpack.config.js with the following content:
    const 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]'
                            }
                        }
                    ]
                }
            ]
        }
    };​
    Notice the css-loader's options property. We set modules to true to enable CSS Modules in Webpack. I'll explain localIdentName property a little bit later.
  4. 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:
  5. 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.js​

    dist/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 :global keyword 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 .container class to variable root by accessing style.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());
  6. 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.

  1. localIdentName property is used to specify the renaming of CSS classnames. Under the hood, css-loader implemented CSS Modules by renaming all classnames, so that they are scoped to their modules. For example, we defined the localIdentName property in webpack.config.js as this:
    options: {
        localIdentName: '[path][name]__[local]--[hash:base64:5]'
    }​
    This configuration would rename the .container class in src/main.css as this:
    src-main__container--A7AOC​
    And the .container class in src/card/main.css would be renamed to this:
    src-card-main__container--1f2By
    With this mechanism, we can use the same classname for different modules. Really cool!
  2. :global keyword is used as an exception to the CSS Modules. The classnames that are preceded by this keyword would not be renamed by css-loader. For example, we use :global for the .card-shadow classname 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!

Idiot Note CSS