Many people who work with React are familiar with the excellent classnames
library. If you aren't familiar, it provides a simple function for gluing classnames together. In web programming in general, there are many times that we need to add or remove multiple classes based on conditional logic. The classnames library makes this easy.
More and more developers are embracing CSS Next and the power of CSS modules. However, when you add CSS modules to your react components, working with classnames gets more difficult. Typically, CSS modules is implemented with class name mangling. Transforming human readable class name strings into unique identifiers helps ensure that every class name in your app is unique.
This means that you can write your component CSS in isolation without worrying about the dreaded class name collisions that have plagued CSS developers since the dawn of the internet. CSS modules are like BEM without all of the hassle. The downside is that applying the class to your components is harder — the class name has been mangled behind the scenes but you still need to use it. We'll see below how CSS modules supplies a styles
object that provides a simple mapping of the human readable class names to the unique versions created by webpack.
Thankfully, the classnames
library ships with an alternative method for binding the classnames
func to the styles
object. This makes it very easy to work with the human readable names without having to wrestle with a mapping object. You can read more about this function in the classnames docs.
The purpose of this document is to showcase some examples of why that function exists, how to use it and how it works.
https://github.com/JedWatson/classnames#alternate-bind-version-for-css-modules
For each of the examples below, presume we're importing the following CSS file as styles
. The styles themselves are unimportant for this example. We're focusing here on the class names that it exports. Discussing the finer details of how CSS modules works is outside the scope of this document. However, at a high-level, presume that webpack will ensure that the each class name in this file is re-written to be totally unique. This avoids any chances of accidental class name collisions and is one of the main benefits of CSS modules.
.something { color: deeppink; }
.selected { font-weight: bold; }
.something-else { color: goldenrod; }
/* remember this! we're laying a trap */
.something-global { color: black; }
/* also make note that we're not adding ".something-local" */
Note: CSS modules can be configured a few different ways. To be backwards compatible, the spec suggests making styles global by default. However, for our purposes, presume we have configured classes to be local by default. Meaning, presume that a class is automatically wrapped in :local()
and that you need to specify :global()
to get an unmangled classname. You can read more about scoping in the webpack css-loader docs.
Remember the past? Before we used CSS modules it was really easy to add classes to stuff. Below we are not importing any styles. We're using classnames
only to deal with the awkward syntax of adding the selected
class. For this example, presume that the styles have been included in some global CSS file and added to the page for us.
Below you can see that it's really easy to set boolean classnames when you strategically name your boolean props. This makes your code very concise and easy to read. You can read more about using classnames in the docs.
import classnames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
const Something = ({ selected }) => {
return (
<div className={classnames('something', { selected })}>
Hello
<span className="global-style something-else">
World
</span>
</div>
);
};
Something.propTypes = {
selected: PropTypes.bool
};
export default Something;
Note: It's important to recognize that the selected
prop isn't required and doesn't have a default value. When designing React components it's a good practice to let undefined values be undefined. If a boolean prop isn't specified then it will be undefined
, which is a falsey value. In that cases, the classnames
func will not apply the selected
class name. So, the absence of the prop means the absence of the class name — pretty cool!
Although classnames
has a somewhat awkward API for setting boolean classnames, it's way better than doing it by hand. A little magic goes a long way.
Below we're demonstrating a sampling of ways that we could construct a boolean classname without using the magic of the classnames
function.
// we could put a ternary in an interpolated string
const classname = `something${ selected ? ' selected' : '' }`;
// we could capture the ternary in a variable (to hopefully enhance readability)
const selectedClassName = selected ? ' selected' : '';
const classname = `something${selectedClassName}`;
// we could use a bitwise operator
const classname = `something${selected && ' selected'}`;
// we could join an array and hope for the best
const classname = ['something', selected && 'selected'].join(' '); // <-- will have an empty trailing space if selected is falsey
Note: We're going to great lengths above to avoid unnecessary spaces between and after class names. This isn't strictly necessary. Unnecessary spaces won't affect rendering in any meaningful way. Many developers, however, prefer to keep things tidy. If you don't care about spaces, array joining is probably the best bet. It's worthwhile to notice how similar the array joining method looks to the classnames
API.
let selected = true;
['something', selected && 'selected'].join(' '); // --> 'something selected'
classnames('something', { selected }); // --> 'something selected'
selected = false;
['something', selected && 'selected'].join(' '); // --> 'something ' (notice the space)
classnames('something', { selected }); // --> 'something' (no space! how tidy!)
Let's look at the same example component from before, except we'll use CSS modules to import our styles. For fun, let's see what it looks like without using the classnames
library. Because we aren't using a helper function, we're forced to use string interpolation (or other tricks) to glue classnames together.
This is probably the most explicit way of doing things. Any developer would be able to read this code and figure out what's going on. There isn't an API to learn; it's all just plain JS. No magic here. The downside is that it's somewhat verbose and slightly ugly because of the syntax of string interpolation.
Bad things:
- This is pretty hard to read compared to the example above.
- Less ugly alternatives, like array joining, add the overhead of creating arrays to create a string.
Good things:
- No specialized API to learn.
- Very straight-forward. Just plain JS.
// <-- not importing classnames
import PropTypes from 'prop-types';
import React from 'react';
import styles from './styles.css'; // <-- CSS modules
const Something = ({ selected }) => {
const selectedClassName = selected ? ` ${styles.selected}` : ''; // <-- captured in a variable because it's too long
return (
<div className={`${styles.something}${selectedClassName}`)}>
Hello
<span className={`global-style ${styles['something-else']}`}>
World
</span>
</div>
);
};
Something.propTypes = {
selected: PropTypes.bool
};
export default Something;
Note: It's important to notice the syntax for styles['something-else']
above. That syntax is the only way to reference object keys with disallowed characters. Because of that awkward syntax, many developers feel a pressure to use camelCased class names in their CSS when using CSS modules. The only problem is that CSS has a long history of preferring dash-cased class names. For seasoned developers, this can be more than a little awkward. It simply looks wrong to see camelCased class names in a CSS file (maybe we should get over it). We'll see below how to alleviate that pressure and avoid going against some deeply ingrained css class name naming dogma.
Let's take a short moment to dissect what CSS modules is returning in the styles
object. Many developers start using the object without really taking a moment to look under the hood. The styles
object is just a key-value pair of the original style name and the mangled style created by webpack. It's good to keep this in mind as we review the examples further below.
Below you'll need to use your imagination an little bit. Webpack can be configured to apply any number of patterns for ensuring unique class names are generated. The exact pattern is up to the developer who configures webpack for your project. Because that configuration is different from project to project, we're simplifying the conversation and using __mangled__
as a prefix to represent the unique class name produced by the webpack css-loader. As a stylistic flourish, we refer to this unique classname as the "mangled" version, to indicate that it has be altered so as to make it unrecognizable.
So, when you see __mangled__something
, imagine that you're looking at a unique string produced by webpack. The webpack css-loader docs show a very unique string like _23_aKvs-b8bW2Vg3fwHozO
to make this same point. For simplicity and readability, we're preferring to use __mangled__
to stand in for that unreadable identifier.
import { isEqual } from 'lodash';
import styles from './styles.css'; // <-- webpack returns an object that maps the original name to the mangled name
// let's create our own object
const fakeStyles = {
something: '__mangled__something', // <-- use your imagination here (presume we're able to guess what webpack would generate)
selected: '__mangled__selected',
'something-else': '__mangled__something-else',
'something-global': '__mangled__something-global', // <-- note: not really global
};
// the styles object is just a key-value map
isEqual(styles, fakeStyles); // --> true
// try it yourself!
console.log(styles); // <-- you can do this right now in the component you're working on!
Note: In the example above, we can see that our something-global
class was actually transformed into a very local class. This is the first part of the trap that we're laying. Also, notice that something-local
was not transformed because it wasn't actually placed in our CSS file. Keep these two things in mind, we'll come back to it further below.
When we add classnames
back to the mix, things get a little bit better. Using classnames cleans up some of the messy string interpolation and completely removes the need to capture the selected classname in a variable (or does it?). However, using the styles
object presents some of its own troubles.
On the surface these issues have nothing to do with classnames; styles
is coming from CSS Modules. All classnames
is doing is gluing strings together for you. The main issue is that we're unable to use the class name string directly. Instead, we need to reference the mangled class name using the styles
mapping object. We'll see in later examples how to solve some of these issues.
You can see in the example below that converting to use CSS modules introduces some awkward syntax.
Bad things:
- Pressure to use camelCased classnames in css. Consider
classnames(styles['something-else'])
versusclassnames(styles.somethingElse)
- Verbose when setting boolean classnames. Consider
classnames({ [styles.selected]: selected }
- Much more verbose than using
classnames
without CSS modules. Rememberclassnames('something-else')
from above? Compare that toclassnames(styles.something)
. - Fails in cases where
styles
is undefined (some edge cases in tests and SSR).
Good things:
- Very explicit about where a specific classname is coming from. No potential for confusion between imported CSS module styles and global styles (more on that later).
- Decidedly easier to read than not using
classnames
at all.
import classnames from 'classnames'; // <-- better than nothing
import PropTypes from 'prop-types';
import React from 'react';
import styles from './styles.css';
const Something = ({ selected }) => {
return (
<div className={classnames(styles.something, { [styles.selected]: selected })}>
Hello
<span className={classnames('global-style', styles['something-else'])}>
World
</span>
</div>
);
};
Something.propTypes = {
selected: PropTypes.bool
};
export default Something;
Note: Using the styles
object directly with classnames
can lead to some very awkward syntax. Consider the next example below.
import classnames from 'classnames';
import styles from './styles.css';
let somethingElse = true;
// pretend we want to conditionally apply 'something-else'
classnames({ [styles['something-else']: somethingElse }); // -> something-else
In the above example, we're using the styles
object every time we need to reference a local classname. This can be quite cumbersome. If you're converting an old component to use CSS modules you can actually increase the files size (and reduce the readability) quite a bit.
Thankfully, classnames
ships with an alternative bind method that's designed specifically to make working with CSS modules much less cumbersome. It requires a small bit of extra set up (and has one important caveat) but it gets us closer to the less verbose syntax we enjoyed in the days before CSS Modules. Of course, there are numerous benefits of using CSS Modules. So, why not make it easy to work with?
Bad things:
- If you see
cx('something')
you will need to refer to thestyles.css
to be sure you're not accidentally referencing a global class (more on this below).
Good things:
- More like using
classnames
without CSS Modules - Using
cx('something')
is significantly fewer characters thanclassnames(styles.something)
- Using
cx({ selected })
is significantly less cumbersome/error prone thanclassnames({ [styles.selected]: selected})
- In situations where
styles
is undefined (some edge cases in tests and SSR), the boundcx
function falls back silently to global classes. - Using the bound
cx
version makes it less painful to use dash-cased strings, considercx('something-else')
versusclassnames(styles['something-else'])
. It essentially removes the pressure to use camelCased strings for classnames.
Note: Falling back to global styles is maybe a good thing or a bad thing... depending on why the styles object was undefined.
import classnames from 'classnames/bind'; // <-- notice bind
import PropTypes from 'prop-types';
import React from 'react';
import styles from './styles.css';
const cx = classname.bind(styles); // <-- explicitly bind your styles
const Something = ({ selected }) => {
return (
<div className={cx('something', { selected })}>
Hello
<span className={cx('global-style', 'something-else')}>
World
</span>
</div>
);
};
Something.propTypes = {
selected: PropTypes.bool
};
export default Something;
Take a moment and refresh your memory on the trap we've been laying. As noted above, it's possible to find an edge case where the bound cx
func will return an unexpected global class name.
Remember also, we're explicitly binding our styles object map to the cx
function. The issue is not a matter of being explicit. Within your file, using cx
or classnames
are equally explicit. Consider cx('something')
in the example above versus classnames(styles.something)
. Both examples demonstrate where the class name comes from.
If you are going to be using the bound cx
func, you will need to keep these traps in mind.
Note: Although the bound cx
function exposes these edge cases, it's really a failing of the developer. You shouldn't be placing global classes in your CSS modules (unless you wrap them in :global()
). And you shouldn't be referencing local classes in your component that aren't actually in your CSS module. Whoops!
Consider this: cx('something-local')
.
If that class name isn't defined in your CSS module (remember our example CSS file above), then an unmangled global classname will be applied instead. Meaning, your <div className={cx('something-local')} />
will be rendered as <div class="something-local" />
. If you were expecting <div class="__mangled__something-local" />
it could lead to unexpected style errors, especially if something-local
is defined in some hidden corner of your global CSS.
Pretend your application includes Twitter Bootstrap CSS. In that case, there will be a .container
class globally available. Imagine that you thought that you had a container class in your CSS module. You might be surprised when the global container CSS gets applied. In the example below, mentally replace .something-local
with .container
.
// Trap 1: an unexpected global
import classnames from 'classnames/bind';
import styles from './styles.css';
// whoops! something-local is undefined
styles['something-local']; // --> undefined
const cx = classname.bind(styles);
// perhaps unepectedly, outputs a *global* class reference
cx('something-local'); // --> "something-local" not "__mangled__something-local"
Note: you need to use your imagination here. This is very likely to show up after a refactor, when a class name declaration was removed from the CSS file but not from the component. In that case, the class name will no longer exist in the styles
import. If, by random chance, that old class name is defined in some globally included CSS that you are unaware of, you might accidentally run into a class name collision.
Also, consider this: cx('something-global')
.
If that class name is defined in your CSS module (again, remember our example CSS file above), then a mangled classname will be used instead! This could also lead to unexpected style errors. Meaning, your <div className={cx('something-global')} />
will be rendered as <div class="__mangled__something-global" />
. If you were expecting <div class="something-global" />
it could lead to unexpected style errors, especially if something-global
is designed to be included from your global CSS.
Again, pretend your application includes Twitter Bootstrap CSS, and you want to use the global .container
class. Imagine that someone has, unexpectedly, added a .container
class to your CSS module. You might be surprised when the global styles aren't applied. In the example below, mentally replace .something-global
with .container
.
// Trap 2: an unexpected local
import classnames from 'classnames/bind';
import styles from './styles.css';
// whoops! something-global is actually local
styles['something-global']; // --> __mangled__something-global
const cx = classname.bind(styles);
// perhaps unepectedly, outputs a *local* class reference
cx('something-global'); // --> "__mangled__something-global" not "something-global"
Traps aside, it's important to look deeper into how the bound cx
function works. Perhaps understanding the inner workings better will make the traps more apparent and easier to avoid.
First thing's first... the bound version of classnames isn't magic. It's a bound selector. To demonstrate the key functionality, consider this toy example where we're creating our own cx
function from scratch.
You can see below some examples of what classnames
, cx
and cxFake
return respectively. You can also see that nothing prevents you from referencing the styles
object directly. It's worth noting that the classnames
func imported from 'classnames/bind'
works exactly the same as importing it from 'classnames'
(except it now has a bind
method).
import { get } from 'lodash';
import classnames from 'classnames/bind';
import styles from './styles.css';
// attempt to read the value from the styles object, return the key by default
const fakeBind = (styles) => (key) => get(styles, key, key); // <-- a curried selector
const cx = classnames.bind(styles);
const cxFake = fakeBind(styles); // <-- "binding" is another name for currying
// regular
classnames('global-style') // --> global-style
classnames('something') // --> something (global)
classnames(styles.something) // --> __mangled__something (local)
// bound
cx('global-style') // --> global-style
cx('something') // --> __mangled__something (local)
cx(styles.something) // --> __mangled__something (oh, cool)
// fake bound
cxFake('global-style') // --> global-style
cxFake('something') // --> __mangled__something (local)
cxFake(styles.something) // --> __mangled__something
Looking deeper at the fakeBind
function, we can see that it is simply returning a property from the styles
object. In cases where that property isn't present on the styles
object, it simply returns the original string. Nothing could be simpler!
The toy example above is missing some functionality from the real classname function. For fun, here's a more complete example that implements the basics of the classnames
and cx
functions, including the ability to specify multiple class names.
// handles boolean class name map objects
const reduce = (styles) => (obj) => {
return Object.keys(obj).reduce((final, key) => {
if (!!obj[key]) { // <-- only include truthy keys
final += ' ' + get(styles, key, key);
}
return final;
}, '');
}
// joins multiple class names and boolean class name maps
// NOTE: for simplicity, this will add extra spaces
const fakeBind = (styles) => (...keys) => {
keys.reduce((final, key) => {
if (typeof key !== 'string') {
final += ' ' + reduce(styles)(key); // <-- presume it's a boolean class name map
} else {
final += ' ' + get(styles, key, key); // <-- attempt to select the class name from the styles map
}
return final;
}, '');
}
const styles = { b: '__mangled__b' }; // <-- let's create our own CSS modules styles map
const cxFake = fakeBind(styles);
cxFake('a', 'b', { c: false, d: true }); // --> a __mangled__b d
Hopefully this overview helps to demystify how using bound styles with classnames
can help simplify your roll out of CSS modules in your React application. Although there is one important pitfall to keep in mind, the greatly reduced syntax is a big win for productivity and readability.
Thank you for such a detailed and simple explanation!