Using BEM conventions in CSS modules leveraging custom webpack loaders
To style our React components we use CSS modules. We write each component styles following the BEM conventions in order to understand at glance the role of each class in the component structure. It also helps us to later extract inner private components ensuring that they can be used elsewhere without further ado.
This combo comes with a little nuisance: we lose the information the BEM naming gives us when referencing the styles in JS. We can fix this by leveraging custom webpack loaders.
In this post I assume you know the BEM naming conventions, what CSS modules are, and how to use them with webpack via the css-loader.
The problem with BEM and CSS modules
Suppose we have this stylesheet:
When imported into our JS file as a CSS module, not all those classes are valid or acceptable JS identifiers, so we may need to use the bracket notation:
We are using the classnames library to easily compose classes, however it’s still ugly and noisy.
The css-loader has support for automatic camelCasing (camelCase
option). With camelCasing enabled, the exported styles will always be acceptable JS keys.
We don’t use the brackets anymore. Unfortunately, we have lost information about what is a block, an element or a modifier. Also we are prone to have className clashing: both button__icon
and button-icon
are camelCased as buttonIcon
.
Webpack loaders to the rescue
A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader.
So a loader is a function that receives text, performs some kind of transformation and returns a new text. We can write our own loader that wraps the css-loader and generate our own and improved camelCased class names.
The css-loader takes the stylesheet and returns a JS module that exports a module.locals
map containing references to the class names.
Our custom loader:
- Gets the css-loader output.
- Finds the
module.locals
and parses them. - Creates a new structure that allows us to access the class names using plain objects and dot notation.
- Changes the original source injecting this new structure into the
module.locals
.
We’ll use the following API to access the class names. Elements are nested inside their parent blocks. Modifiers are prepended with a $
(although this is a configurable setting).
Below is the loader code. Notice that we need to use objects with toString
methods to achieve the desired API. This is done in two passes: we serialize the object structure first and them replace our toString
placeholders with an arrow function. It’s not beautiful but it does the job.
To use the loader we just add it to the webpack pipeline.
And here is the final component code.
The classnames library caveat
Did you notice the cn
import change in the last example?
The loader approach works nicely with React but does not play well with the classnames library due of how it handles parameters. It has a hard time distinguishing between optional classes and the “bemified” structures we use. If you want to use it you must implement a custom version with a less flexible API. It’s simple and does the job:
The prop-types warning
Following the same logic we’ll soon find that using propType.string
for the classname
prop issues a warning: we must use a custom propType. By default we usually re-export the prop-types
library to add our own prop-types with ease:
Testing with Jest and enzyme
As you can imagine testing components that rely on this loader is another challenge. Jest and enzyme won’t work out of the box. To test this components we need to customize a Jest moduleNameMapper and a enzyme serializer. In another post: “Testing CSS modules in React components with Jest, Enzyme and a custom moduleNameMapper” I cover this topic in detail. You can see both the mapper and the serializer we use in this gist.
Closing thoughts
Our custom webpack loader is highly coupled with the css-loader and performs a series of text based operations. Changes in the css-loader output format poses a threat to our loader code.
Instead of relying on the loader, we could have been explicit and “bemify” the styles in each component:
Despite the coupling, I personally prefer to avoid that kind of boilerplate code. It’s easy to miss and waste time debugging just to realize you forgot to call the utility function. We can let webpack do the work for us and stop worrying.
We must always also understand the tradeoffs derived from the use of new abstractions and accept them. In this scenario, using a notation easier on the eye forced us to add a new loader, a custom classnames implementation, a custom prop-type, and to tweak the tests. Is it worth the effort? That’s something you must decide on your own.