Embracing Constraint with CSS Modules

Matt Seccafien
cartogram
Published in
7 min readSep 20, 2016

A snippet of recent experiences.

At Cartogram, we have some large projects that we revisit in sprints every year, and each time part of the budget is spent on refactoring. Typically this effort entails either modernizing/unifying the user interface design, removing/rewriting old javascript code, updating packages somewhere in the tool-chain, or (with much anxiety and extreme grimacing) editing/removing CSS.

On a recent project refactor, the CSS-bloat was weighing heavy on our conscience. This project was originally built using the first version of Zurb’s Foundation, and has only grown in complexity over the past 6 years to support new features in the UI.

Rather than dredging through old .scss files or waiting for the green light on a complete overhaul, it was time for something drastic. A new feature we’ve been developing was using React extensively so we decided to use CSS Modules to apply the style to just this new feature.

I was hesitant, CSS Modules are a relatively new thing and have been praised and criticized a lot. In addition, the learning curve makes them feel limiting at first, especially when coming from a feature-rich preprocessor.

Limitations are good! This has been a popular soundbite in the JavaScript community over the last year with the simplicity of Redux gaining almost total dominance in the state management area of our applications.

Constraints turn out to be just as important as features.
— Dan Abramov via

My goal is to briefly explain how to embrace the constraints of CSS Modules as well as a few things I wish I had known from the start that allowed us to use CSS Modules successfully.

The Obligatory Button

Buttons are a good place to start because they require many styles applied conditionally. Most interfaces will have several different types of buttons with various states.

Focusing on this element I will demonstrate how to add some basic styles using CSS Modules and will also touch on how to augment that with more complex style control.

Adding Basic Styles

One of my favourite things about CSS Modules is the co-location of component-specific styles with functionality and markup. This is one of the reasons people advocate for styling with CSS inside of JavaScript files, but being within the context of an actual .css file constrains us to CSS standards (with media queries, :hover, :active, :focus states, etc…) rather than having to reinvent features or articulate style in a new JavaScript way (it’s margin-right not marginRight, #ammiright!?).

Current convention promotes the following structure where we have all our Button component files in a directory named with a capital. Ideally you would have a Readme.md, package.json and your tests files in this directory as well.

/Button
- index.js
- styles.css
- Readme.md
- package.json
+ /tests

Using this structure, let’s import the styles.

import styles from './styles.css';

And assuming we’ve defined a .Button {…} style block in that file, it will be imported and applied by adding className={styles.Button}.

/* Button/styles.css */.Button {
color: blue;
border: 1px solid;
}
/* Button/index.js */import styles from './styles.css';/* ... */<a
{...other}
href={href}
onClick={onClick}
disabled={isDisabled}
className={styles.Button} /* Applying the styles */
>
/* ... */

And that’s it, but before we get too far ahead I want to quickly cover a few basic things we should know about CSS Modules. If you’d like a deeper dive into the basics of CSS Modules I suggest reading this article on CSS-Tricks.

  • CSS Modules need to be piped though a bundler (usually webpack with the css-loader). This is definitely a barrier to entry, but requiring a compilation step before we can run our code in the browser is the unfortunate reality of web-development today. The benefit is that we can append other plugins like PostCSS/CSSNext to this process. Take a look at this repo for more information on setup.
  • When the bundler comes across an import statement pointing to a CSS file, an object is created that maps the style blocks in the CSS file to dynamically scoped class names that we attach to components in the JavaScript file (using the className prop). Take a look at this blog post for more information on this.
  • All styles are local to the file that imports them. If you want a block to be global, you need to specify this with :global(.globalClassName) {…}.
  • You can use the composes keyword to combine the styles from one style block into another.
.base {
box-sizing: border-box;
margin: 0;
padding:0;
}
.component {
composes: base;
border: 5px solid red;
/* ... other styles */
}
  • You can compose from another file.
.component {
composes: base from '../Base/styles.css';
border: 5px solid red;
/* ... other styles */
}
  • You can use value to assign local variables
@value radius: 4px;.component {
border-radius: radius;
}
  • You can import one or multiple values from other files.
@value colors: '../styles/colors.css';
@value primary, secondary, tertiary from colors;
/* or like this */@value large as bp-large from "../styles/breakpoints.css";.component {
color: primary;
border-color: secondary;
background: tertiary;
}
@media bp-large {
.component {
/*.. large screen sizes only */
}
}

With that primer out of the way, lets continue styling our Button.

Multiple Class Names

We’ve already added a single class and style block to the component, but if we want to apply multiple classes and style blocks we can use the classnames module to combine them.

/* Button/styles.css */.Button {
color: blue;
border: 1px solid;
}
.ButtonIsDisabled {
opacity: 0.5;
}
/* Button/index.js */import classnames from 'classnames';/* ... */<a
/* ... other props */
className={classnames(
styles.Button,
styles.ButtonIsDisabled
)}
>
/* ... */

Adding Conditional Styles

The above snippet combines the styles, but we only want to apply the ButtonIsDisabled style when the isDisabled prop is true. We can achieve this using a special bind version of the classnames module, and passing our entire styles object to the imported function.

import styles from ‘./styles.css’;
import classnames from ‘classnames/bind’;
const cx = classnames.bind(styles);
/* ... */const cls = cx({
Button: true,
ButtonIsDisabled: isDisabled, /* only if isDisabled === true
});
/* ... */<a
/* ... other props */
className={cls} /* Apply the combined style blocks */
>

Using BEM Naming

By using CSS Modules, you do not need to use BEM naming. All styles are local by default and the actual class names in the resulting markup are mangled to be unique. We no longer need to prefix each block with the component we are working on.

However, I like using the BEM naming convention. Naming is hard. I am now (as many are) disciplined in BEM and value the extra thought it forces me to put into the names of my selectors. I am not ready to abandon this. If you also prefer to write .Button {…} (for the root component) and .Button — is-disabled {…} for the disabled state, you can achieve this by wrapping the style key in square brackets and back ticks.

const cls = cx({
Button: true,
[`Button--is-disabled`]: isDisabled,
});

Dynamic Class Names

I’ll admit it isn’t the prettiest, but this syntax also allows us to apply classes that are named using the template literal. Suppose, for example, we have buttons with different rounded corners across our UI, a common need if you are using groups of buttons that are connected to each other. We can achieve this quite simply by adding to our style object.

const { 
/* ... other props */
isDisabled,
radius,
} = props;
const cls = cx({
Button: true,
[`Button--is-disabled`]: isDisabled,
[`Button--radius-${radius}`]: radius,
});
Button.propTypes = {
/* ... other props */
radius: React.PropTypes.oneOf([
'none',
'all',
'left',
'right',
'bottom',
'top'
]),

};

Now our radius class will only be applied if we have a radius prop, and the class will be generated using that same value. We can now define these styles in Button/styles.css.

/* ... other styles */.Button--radius-all {
border-radius: 4px;
}
.Button--radius-left {
border-radius: 4px 0 0 4px;
}
.Button--radius-right {
border-radius: 0 4px 4px 0;
}
.Button--radius-top {
border-radius: 4px 4px 0 0;
}
.Button--radius-bottom {
border-radius: 0 0 4px 4px;
}

Overriding Styles

We also need a way for the parent component to add any style customizations in place as our Button component is called. Typically this will be for minor overrides, such as positioning inside a layout component. We can achieve this with one slight modification to our style object that will allow the parent to pass down a style block in a className prop.

const { 
/* ... other props */
className,
} = props;
const cls = cx(className, {
Button: true,
[`Button--is-disabled`]: isDisabled,
[`Button — radius-${radius}`]: radius,
});

We can now apply style in a parent component, such as a Form.

/* Form/styles.css */.FormSubmitButton {
width:100%;
}
/* Form/index.js */
import styles from './styles.css';
/*...*/<Button
radius="all"
className={styles.FormSubmitButton}
isDisabled={this.props.store.submissionInProgress}
>
{buttonText}
</Button>

In a more complex example, we revisit the idea of a ButtonGroup component that adds the correct radius prop on each end of the array of Button child components.

In Conclusion

CSS Modules can be a very effective method for style composition in React. Specifically, it constrains us to CSS Standards while also generating locally-scoped styles that have straight-forward mappings to functionality and markup.

In our case, CSS Modules allowed us to add complex features to an existing code base without leaking styles into the rest of the project. This ability for CSS Modules to co-exist with the legacy styles is a big advantage. Being able to selectively adopt a new tool or technique within a larger project is a great way to reduce the need for a complete overhaul. It can also help set the stage for new features to follow, and eventually work its back way across the entire project.

You can certainly achieve the same results using your preferred flavour of React Style composition (inline JS styles, Radium, etc…), but I like this approach because it ships the smallest amount of CSS with each component while still allowing me to write styles in CSS files (something I am not ready to give up despite understanding the benefits of the CSS inside JavaScript methodology).

If you have comments or suggestions, please send me an email. Cheers!

--

--

Matt Seccafien
cartogram

Designer, developer and co-founder of @studiocartogram and @fondfolio