jahed.dev

Using CSS Modules with BEM

CSS Modules are often touted as the next step from CSS methodologies like BEM (Block Element Modifier). In this article, I'll explain why BEM should still be used alongside CSS Modules for consistent and intuitive styling.

A Quick Overview

What is BEM?

BEM essentially provides guidelines to structure your CSS selectors in a way that allows you to reuse components (blocks) and manage state changes (modifiers). By clearly defining your components, you have essentially namespaced your CSS to avoid naming conflicts.

/* styles.css */
.link {
  color: black;
}

.link--active {
  color: blue;
  font-weight: bold;
}
<!-- index.html -->
<a class="link link--active">Home</a> <a class="link">About</a>

Here we have a navigation where the "Home" link is the current page, so we make it blue and bold. This overriding relies on the order of class names, the later class names overriding styles of the previous. So the black turns to blue.

What are CSS Modules?

CSS Modules builds on this and provide scoped selectors. Selectors no longer need to be global, and so we can avoid selector conflicts between components. A pre-processor can take these scoped class names and generate non-conflicting global class names, which can be used on your components.

/* Link.css (source) */
.base {
  color: black;
}

.active {
  color: blue;
  font-weight: bold;
}
/* Link.css (generated css) */
.base-2xc983 {
  color: black;
}

.active-dfkj39 {
  color: blue;
  font-weight: bold;
}
// Link.css (generated mappings)
{
  "base": "base-2xc983",
  "active": "active-dfkj39"
}
// Link.jsx
import React from "react";
import classnames from "classnames";
import styles from "./Link.css";

export const Link = ({ active, children }) => (
  <a
    className={classnames(styles.base, {
      [styles.active]: active,
    })}
  >
    {children}
  </a>
);
// index.jsx
import React, { Fragment } from "react";
import { Link } from "./Link.jsx";

render(
  <Fragment>
    <Link active>Home</Link>
    <Link>About</Link>
  </Fragment>
);
<!-- HTML Result -->
<a class="base-2xc983 active-dfkj39">Home</a> <a class="base-2xc983">About</a>

Because CSS Modules rely on mapping source class names to generated class names, we'll need to use JavaScript to dynamically inject it into the DOM. In this example we're using React, css-loader and classnames to handle the rendering.

Notice how the selectors in this example are completely generic. They're allowed to be since they're locally scoped and won't conflict with other CSS Modules which use the same selectors.

CSS Modules without BEM

If you're not using BEM when naming your class names in CSS Modules, you're back to the age-old problem of deciding a suitable naming scheme for your class names. BEM was made to solve this so there's really no reason not to keep using it.

Say we wanted to add another link, "Sign In" which looks like a button. We can do the following without BEM:

/* Link.css (source) */
.base {
  color: black;
}

.active {
  color: blue;
  font-weight: bold;
}

.button {
  display: inline-block;
  padding: 4px;
  background: blue;
  color: white;
}

.button.active {
  background: white;
  color: blue;
}
// Link.css (generated mappings)
{
  "base": "base-2xc983",
  "active": "active-dfkj39",
  "button": "button-cvbn31"
}
// Link.jsx
import React from "react";
import classnames from "classnames";
import styles from "./Link.css";

export const Link = ({ active, type, children }) => (
  <a
    className={classnames(styles.base, {
      [styles.active]: active,
      [styles.button]: type === "button",
    })}
  >
    {children}
  </a>
);
// index.jsx
import React, { Fragment } from "react";
import { Link } from "./Link.jsx";

render(
  <Fragment>
    <Link active>Home</Link>
    <Link>About</Link>
    <Link type="button">Sign In</Link>
  </Fragment>
);
<!-- HTML Result -->
<a class="base-2xc983 active-dfkj39">Home</a> <a class="base-2xc983">About</a>
<a class="base-2xc983 button-cvbn31">Sign In</a>

We can see the issue here. Every time we introduce a new "type" of Link, we'll need to add another conditional property to the "className" to pick out the right class name from our CSS Module. We can figure out a naming scheme so that we can automatically use the right class name, or we can just use BEM.

CSS Modules with BEM

Because BEM's naming scheme uses Blocks, Elements and Modifiers, we can remap these concepts to React. Blocks are Components, Elements are Child Components and Modifiers are Props/State.

So "Link" is our Block, "active" and "type" are our modifiers where:

Here is the same example, but using BEM instead:

/* Link.css (source) */
.Link {
  color: black;
}

.Link--active {
  color: blue;
  font-weight: bold;
}

.Link--type--button {
  display: inline-block;
  padding: 4px;
  background: blue;
  color: white;
}

.Link--type--button.Link--type--active {
  background: white;
  color: blue;
}
/* Link.css (generated mappings) */
{
  "Link": "Link-2xc983",
  "Link--active": "Link--active-dfkj39",
  "Link--type--button": "Link--type--button-cvbn31"
}
// Link.jsx
import React from "react";
import { bemModule } from "@jahed/bem";
import styles from "./Link.css";

const bem = bemModule(styles);

export const Link = ({ active, type, children }) => (
  <a className={bem("Link", { active, type })}>{children}</a>
);
// index.jsx
import React, { Fragment } from "react";
import { Link } from "./Link.jsx";

render(
  <Fragment>
    <Link active>Home</Link>
    <Link>About</Link>
    <Link type="button">Sign In</Link>
  </Fragment>
);
<!-- HTML Result -->
<a class="Link-2xc983 Link--active-dfkj39">Home</a>
<a class="Link-2xc983">About</a>
<a class="Link-2xc983 Link--type--button-cvbn31">Sign In</a>

In the example above, I'm using my own library "@jahed/bem" which has a "bemModule()" function which:

This way, we can avoid having to rewrite every class name and keep them synchronised. If a modifier doesn't exist in the CSS Module, it's simply ignored.

Conclusion

There are a number of advantages to using CSS Modules with BEM and generated class name assignments. Here's a few:

@jahed/bem is an open source project, so feel free to contribute with issue tickets and pull-requests.

If you want to know more about the library and how it came to be, I've written a separate article for it.