jahed.dev

@jahed/bem, A Brief History

@jahed/bem is a simple library I pulled out from one of my existing projects to demonstrate why it's beneficial to use BEM with CSS Modules.

Moving over to this approach has made the dependency between my components and stylesheets a lot less complex. I'll be going through this migration to give some context to my conclusions.

Using BEM with React

Before using CSS Modules, I was already using BEM, but struggled to keep my selectors the same since it was a manual process. Sometimes I'd forget an underscore ('_'), a hyphen ( '-') or my modifiers would go out of sync with the component's prop names. So I introduced "bem()". A function to generate these class names to reduce human error.

function generateClassNames(elementName, modifiers = {}) {
  const classes = Object.keys(modifiers)
    .filter((key) => !!modifiers[key])
    .map((modifierType) => {
      const modifierValue = modifiers[modifierType];
      return isBoolean(modifierValue)
        ? `${elementName}--${modifierType}`
        : `${elementName}--${modifierType}--${modifierValue}`;
    });
  classes.unshift(elementName);
  return classes;
}

function bem(elementName, modifiers) {
  return generateClassNames(elementName, modifiers).join(" ");
}

This can then be imported and used by any component.

import React from "react";
import { bem } from "@jahed/bem";
import styles from "./Link.css";

const Link = ({ active, type, children }) => (
  <a className={bem("Link", { active, type })}>{children}</a>
);

export default Link;

A Short Miss-Step

Once I started reading up on CSS Modules, I initially made the mistake of assuming the best way of naming selectors was by state and only state. Most of the examples were written like this and recommended it over BEM. Shortly after, I realised I was reinventing BEM, so I rolled back.

My dislike for the non-BEM approaches may be biased due to the fact that I'm already familiar with BEM. But I do feel there's no real negatives to using BEM over custom naming schemes in CSS Modules.

Migrating to CSS Modules

I already had stylesheets per-component, using BEM, so converting them to CSS Modules was a matter of switching it on in Webpack.

To import the mappings, since all my components were using bem() already, I just had to introduce another layer which takes the mappings and returns the same bem() API. And so, bemModule() was born.

function generateClassNames(elementName, modifiers = {}) {
  const classes = Object.keys(modifiers)
    .filter((key) => !!modifiers[key])
    .map((modifierType) => {
      const modifierValue = modifiers[modifierType];
      return isBoolean(modifierValue)
        ? `${elementName}--${modifierType}`
        : `${elementName}--${modifierType}--${modifierValue}`;
    });
  classes.unshift(elementName);
  return classes;
}

function join(...classNames) {
  return (Array.isArray(classNames[0]) ? classNames[0] : classNames)
    .filter(identity)
    .join(" ");
}

function bemModule(styles) {
  return (elementName, modifiers) =>
    join(
      generateClassNames(elementName, modifiers).map(
        (className) => styles[className]
      )
    );
}

And like bem(), it can be used by any component.

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>
);

Conclusions

So the creation of bem-modules was pretty natural. As such, it's specific to my needs as of writing. However, it may become more flexible if there's a demand for it. Here's some thoughts I've had for its future.

A Slightly Better API?

If my components weren't already using the old bem() API, which looks like this:

type bem = (elementName, modifiers) => string;

type bemModule = (styles) => bem;

I could've used a slightly less repetitive API looking like this:

type block = (modifiers) => string;

type bem = (elementName, styles) => block;

But the benefits are so minor, I can't justify the change.

React Higher Order Component

When using the library with React, you need to explicitly pass the results down to the "className" prop, every time. This can easily be solved using a Higher Order Component. So given these APIs:

type BlockComponent = (props) => React.Element;

type block = (styles) => (React.Component) => (modifiers) => React.Element;

Here BlockComponent is a wrapper which generates the className using a bemModule(React.Component.name, styles)(props) and creates the given React.Component with the className assigned and props forwarded.

We could then use it in this way:

const MyComponent = ({ className, children }) => (
  <div className={className}>{children}</div>
);

export default block(styles)(MyComponent);

While convenient, it adds another layer of complexity. For example, how would you assign Element classNames (for child components)? It would require more functions and/or parameters. Or the user will need to ensure each Element is its own Component wrapped in the higher-order component, which is a lot more wiring than before.

Differing Naming Conventions

While BEM is technically just a methodology. There are multiple naming conventions for it. Currently, the library only support the convention I use. That is:

Block__Element--modifierKey--modifierValue

Supporting other naming conventions is straight forward and I may introduce the capability if there's demand for it.

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