Practical Guide to React and CSS Modules

Practical Guide to React and CSS Modules

Web developers have spent tremendous time and effort to make reusable components. One of the problems has been CSS and its cascading nature. For example, if a developer creates a component that displays a tree structure, how can she/he ensure that CSS class (say, .leaf) used in the component will not have side-effects in other parts of the application. New methodologies and conventions have been created to tackle selector issues. BEM and SMACSS are both widely used methodologies that have served web developers well, but they are far from ideal solutions. This blog post will explain what flaws naming convention–based methodology has, what CSS Modules are, and how these modules can be used in a React application.

The problem with cascading

I am creating a reusable select list (aka drop-down list) as an example of global style issues. Styling the <select> element itself doesn't feel right because there might be a need for unstyled native element or completely different styling in other areas of the website. Instead, I use BEM syntax to define classes:

.select {}
.select__item {}
.select__item__icon {}
.select--loading {}

If I created a new class called item without the select__ suffix, my team might be in trouble if someone else wanted to use the common name item. It will not matter whether a third-party library or a team member is creating the CSS framework for the project. Using BEM helps you avoid this problem by indicating the context select.

The BEM syntax represents a step towards components, as the "B" in BEM stands for "Block," and blocks can be thought of as light-weight components. Select is a component that has different states (select--loading) and children (select__item).

Unfortunately, using a naming convention and thinking in terms of components doesn't solve all the selector issues. Avoiding a name collision is still not guaranteed, and the verbosity of the naming scheme increases the risk of typos and requires a disciplined team where everyone understands the conventions 100% accurately. Typos include having single dash instead of two, mixing modifier (--) and block (__), etc.

CSS Modules to the rescue

The definition of "CSS module" is as follows:

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

The key thing is scoped locally.

To illustrate this concept, let's create JavaScript and CSS files that will define a component.

/* select.css */
.select {}
.loading {}
.item {}
.icon {}
/* select.js */
import styles from "./select.css";

console.log(styles.select, styles.loading);

Simple example, but there are many things happening under the hood.

The CSS file has a lot less noise than the BEM version because it doesn't contain suffixes and extra characters to repeat the context. Why is it that I could remove a suffix such as .select-- without causing problems?

The import statement in the JavaScript file loads the CSS file and converts it to an object. In the next chapter, I'll show you how to setup the build environment to support importing CSS files.

Each class name from the CSS file is a property of the object, in the example above, styles.select, styles.icon, etc.

If the property name is the class name, then what is the value of that property? It is a unique class name, and the uniqueness ensures that styles don't leak into other components. Here is an example of a hashed class name: _header__1OUvt.

You might be thinking, "That looks awful." What is the point of changing meaningful class names to cryptic code?

The main point is that such an identifier is guaranteed to be globally unique. Later in the guide, we will modify identifier creation so that it will have a more human readable identifier but still be unique.

The key benefits of having locally scoped CSS:

  • one step towards modular and reusable components that will not have side effects
  • cleaner CSS
  • avoidance of monolithic CSS files, as each component will have its own file

Disadvantages:

  • not as human-readable DOM
  • a bit of initial preparation to get the build step working

CSS Modules require a build step, but the good news is that various bundling tools support that step for both the client and server JavaScript source code. You can also use CSS Modules with most of the UI libraries.

To keep things simple, in this blog post I'll focus on the Webpack module bundler and React.

React, Webpack, and CSS Modules

To get started without hassle, I use the Create React App.

By following the instructions in the documentation, I get a new project up and running in no time.

npm install -g create-react-app

create-react-app css-modules
cd css-modules/
npm start

lo and behold, we have a React app running:

Create React App start page

The start page says that I should edit the file App.js.

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

Is the Create React App using CSS Modules? I can verify that it is by looking at the file App.js. The CSS file is being imported, but it is not assigned to any variable, and all the className definitions are using strings instead of dynamic values.

At this point, the Create React App isn't supporting CSS Modules, so I need to change the configuration to enable support.

Configure the Create React App to support CSS Modules

To access the underlying build configuration, I need to run the eject command. Note: there is no going back after you do this.

npm run eject

I can now see the config folder and webpack configuration files.

Create React App after eject

The Create React App is using webpack for all assets, so webpack.config.dev.js is the correct configuration file to modify.

I look for a section that defines what to do with the CSS files.

{
  test: /\.css$/,
  loader: 'style!css?importLoaders=1!postcss'
},

Changing it to the following will break the styling of the site for a moment because the configuration will enable CSS Modules, but the component is not changed to support CSS Modules. While I was modifying the webpack configuration, I changed the naming convention to have both human-readable part in it and the hash to keep it unique:

{
  test: /\.css$/,
  loaders: [
    'style',
    'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]&sourceMap&-minimize'
  ]
},

Notice that I changed loader to loaders to give the configuration section an array instead of a single string.

What are those loaders doing? The file webpack.config has a comment section that describes style and CSS loaders:

"style" loader turns CSS into JS modules that inject <style> tags.
"css" loader resolves paths in CSS and adds assets as dependencies.

The modules keyword next to the css-loader enables CSS Modules. The setting localIdentName changes the generated class name so that it contains a React component name, a class name, and, the unique hash id. This configuration change will make debugging much easier because you will be able to identify the problematic component.

Use CSS Modules in React

I can verify that the configuration works by adding a console.log statement to the import statement.

Changing import './App.css'; to

import styles from './App.css';

console.log(styles);

I get the following output to the browser console:

Screenshot of a browser developer tools showing CSS modules hashed class names

The classes are now unique, but they are not used in the React components. There are two steps that I have to follow to get styles applied to the React components. First, change the names of the styles to camel case. Second, change the classNames attributes so that they use imported classes.

It is not mandatory to use camel case capitalization, but, when accessing classes programmatically, it is easier to access styles.componentName than styles["component-name"].

This is the original style file:

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 80px;
}

.App-header {
  background-color: #222;
  height: 150px;
  padding: 20px;
  color: white;
}

.App-intro {
  font-size: large;
}

@keyframes App-logo-spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

There is no need for the App prefixes, so now is a good time to remove those as well.

.app {
  text-align: center;
}

.logo {
  animation: logoSpin infinite 20s linear;
  height: 80px;
}

.header {
  background-color: #222;
  height: 150px;
  padding: 20px;
  color: white;
}

.intro {
  font-size: large;
}

@keyframes logoSpin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

The second step is to change the class usage. The result looks like this:

import React, { Component } from 'react';
import logo from './logo.svg';
import styles from './App.css';

class App extends Component {
  render() {
    return (
      <div className={ styles.app }>
        <div className={ styles.header }>
          <img src={logo} className={ styles.logo } alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className={ styles.intro }>
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

I have now made all the required configuration changes and changed the component so that it uses CSS Modules.

How to break CSS Module boundaries when needed

The setup described in the previous section works as a solid base for a React project, but developers tend to soon realize that they need a way to share styles. In this context, "sharing" means explicitly indicating that a component should inherit something from the base styles.

Shared information can be variables (colors, font sizes, etc.), helpers (SASS mixins), or utility classes.

CSS Modules have an extend functionality that can be used across files with the from keyword.

In the following example, I have two files: one for button base styles and one for the submit button implementation. I can tell that the class submitButton should be composed of base button styles.

/* base_button.css */
.baseButton {
  border: 2px solid darkgray;
  background-color: gray;
}

/* submit_button.css */
.submitButton {
  composes: baseButton from "./base_button.css";
  background-color: blue;
}

The previous example was about CSS classes. If variables are necessary, then you can either use a preprocessor like SASS or Less or add variable support to your webpack.

The example in the variable support documentation:

/* variables.css */
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;

/* demo.css */

/* import your colors... */
@value colors: "./variables.css";
@value blue, red, green from colors;

.button {
  color: blue;
  display: inline-block;
}

I would make a tiny change to the variable naming: use SASS syntax for variable names that are preceded by a dollar sign. The reason for this change is that overriding standard values like blue makes the CSS file less understandable because you can never be sure whether the value has been overridden.

Changed example:

/* variables.css */
@value $blue: #0c77f8;
@value $red: #ff0000;
@value $green: #aaf200;

/* demo.css */

/* import your colors... */
@value colors: "./variables.css";
@value $blue, $red, $green from colors;

.button {
  color: $blue;
  display: inline-block;
}

Conclusion

In this guide, I started with the problem with global CSS and explained why CSS Modules improve the situation by introducing scoped CSS and pushing us towards component-based thinking. Also, I went through the way to easily start experimenting with CSS Modules using the React Starter Kit.

If CSS Modules are used in the backend build step, browser support is not an issue. Browsers receive regular CSS from the server, so there is no way to negatively affect user experience with CSS Modules. Instead, we improve user experience by decreasing the risk of broken layout. The webpack with loaders configured to accept CSS Modules hasn't caused any issues, so I would give it a thumbs up without hesitation.

If you have tried CSS Modules, I would like to hear your experiences!

Discuss on Hacker News