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:
- Modifiers that are undefined, null or false are not applied.
- Modifiers that are boolean and true are applied by key. e.g. "Link --- active"
- Modifiers that are strings, numbers, etc. are applied by key and value. e.g. "Link --- type --- button"
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:
- Wraps the generated class names.
- Returns a function to generate a className string by remapping a given state to the generated class names. I've assigned this to "bem".
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:
- Enforced BEM conventions. Since mappings are generated, selectors will only work if they follow the expected naming scheme. We can go further and show warnings when CSS Modules have invalid selectors (using bemModule()) and when states aren't assigned styles (using bem)
- Consistency and Brevity. Both React components and CSS modules are now a "function" of their state. There's no need for logic in between.
- Component Themes and Composition. Since we use consistent naming, we can take a CSS Module as a component prop instead of using an import.
- Easier Debugging. Since the component name and state is visible in the DOM during Development, it's easier to see the current state in a Web Inspector and find the available states.
- Easier Search and Discovery. Assuming components are named somewhat uniquely, it's easy to run global searches for a given selector rather than binding selectors to where they're imported.
- All the benefits of CSS Modules. Locally scoped selectors and shorter generated selectors in Production stylesheets.
- All the benefits of BEM. Consistent and intuitive naming.
- Not limited to React. bemModule returns strings so you can use the library for server-side templates (Handlebars, EJS) and other frameworks.
@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.