CSS Modules
Welcome to the Future
If you wanted to identify an inflection point in the recent development of CSS thinking, youโd probably pick Christopher Chedeauโs โCSS in JSโ talk from NationJS in November, 2014. It was a watershed moment that set a bunch of different minds spiralling off in their own directions like particles after a high-energy collision. For instance, React Style, jsxstyle and Radium are three of the newest, cleverest, and most viable approaches to styling in React and all reference it in their project Readme. If invention is a case of exploring the adjacent possible, then Christopher is responsible for making a lot of whatโs possible more adjacent.
These are all legitimate problems that affect most large CSS codebases in one way or another. Christopher points out that these all have good solutions if you move your styling to JavaScript, which is true but introduces its own complexities and idiosyncrasies. Just look at the range of approaches to handling :hover
states among the projects I referenced earlier, something that has been solved in CSS for a long time.
The CSS Modules team felt we could attack the problem head-on โ keep everything we liked about CSS, and build upon the good work that the styles-in-JS community was producing. So, while weโre bullish about our approach and firmly defend the virtues of CSS, we owe a debt of gratitude to those folks pushing the boundaries in the other direction. Thanks, friends! ๐ฌ๐ซ๐ญ
Let me tell you about what CSS Modules is, and why itโs the future.
Step 1. Local by default.
In CSS Modules, each file is compiled separately so you can use simple class selectors with generic names โ you donโt need to worry about polluting the global scope. Letโs say we were building a simple submit button with the following 4 states.
Before CSS Modules
We might code this up using Suit/BEM-style classnames & plain old CSS & HTML like so:
/* components/submit-button.css */
.Button { /* all styles for Normal */ }
.Button--disabled { /* overrides for Disabled */ }
.Button--error { /* overrides for Error */ }
.Button--in-progress { /* overrides for In Progress */
<button class="Button Button--in-progress">Processing...</button>
Itโs quite good, really. We have these four variants but BEM-style naming means we donโt have nested selectors. Weโre starting Button
with a capital letter so as to (hopefully) avoid clashes with any of our previous styles or any dependencies weโre pulling in. And weโre adopting the --modifier
syntax to be clear that the variants require the base class to be applied.
All in all, this is reasonably explicit & maintainable code, but it requires an awful lot of cognitive effort around naming discipline. But itโs the best we can do with standard CSS.
With CSS Modules
CSS Modules means you never need to worry about your names being too generic, just use whatever makes the most sense:
/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */
Notice that we donโt use the word โbuttonโ anywhere. Why would we? The file is already called โsubmit-button.cssโ. In any other language, you donโt have to prefix all your local variables with the name of the file youโre working on โ CSS should be no different.
Thatโs made possible by the way CSS Modules is compiled โ by using require
or import
to load the file from JavaScript:
/* components/submit-button.js */
import styles from './submit-button.css';
buttonElem.outerHTML = `<button class=${styles.normal}>Submit</button>`
The actual classnames are automatically generated and guaranteed to be unique. CSS Modules takes care of all that for you, and compiles the files to a format called ICSS (read my blog post about that), which is how CSS and JS can communicate. So, when you run your app, youโll see something like:
<button class="components_submit_button__normal__abc5436">
Processing...
</button>
If you see that in your DOM, that means itโs working!
Naming conventions
Considering our button example again:
/* components/submit-button.css */
.normal { /* all styles for Normal */ }
.disabled { /* all styles for Disabled */ }
.error { /* all styles for Error */ }
.inProgress { /* all styles for In Progress */
Notice that all the classes are stand-alone, rather than one being the โbaseโ and the rest being โoverridesโ. In CSS Modules each class should have all the styles needed for that variant (more on how that works in a minute). It makes a big difference to how you use these styles in JavaScript:
/* Don't do this */
`class=${[styles.normal, styles['in-progress']].join(" ")}`
/* Using a single name makes a big difference */
`class=${styles['in-progress']}`
/* camelCase makes it even better */
`class=${styles.inProgress}`
Of course, if you get paid by the keystroke, do what you want!
A React Example
Thereโs nothing about CSS Modules thatโs React-specific. But React gives you a particularly excellent experience using CSS Modules, so itโs worth showing a slightly more complex example:
/* components/submit-button.jsx */
import { Component } from 'react';
import styles from './submit-button.css';
export default class SubmitButton extends Component {
render() {
let className, text = "Submit"
if (this.props.store.submissionInProgress) {
className = styles.inProgress
text = "Processing..."
} else if (this.props.store.errorOccurred) {
className = styles.error
} else if (!this.props.form.valid) {
className = styles.disabled
} else {
className = styles.normal
}
return <button className={className}>{text}</button>
}
}
You can use your styles without ever worrying about what global-safe CSS classnames are being generated, which lets you focus on the component, not the styling. And once youโre rid of that constant context-switching, youโll be amazed you ever put up with it.
But thatโs just the start. When it is time to think about how your styles are put together, CSS Modules has your back.
Step 2. Composition is everything
Earlier I mentioned that each class should contain all the styles for the button in each different state, in contrast to BEM where it assumes youโd have more than one:
/* BEM Style */
innerHTML = `<button class="Button Button--in-progress">`
/* CSS Modules */
innerHTML = `<button class="${styles.inProgress}">`
But wait, how do you represent shared styles between all the states? The answer is probably CSS Modulesโ most potent weapon, composition:
.common {
/* all the common styles you want */
}
.normal {
composes: common;
/* anything that only applies to Normal */
}
.disabled {
composes: common;
/* anything that only applies to Disabled */
}
.error {
composes: common;
/* anything that only applies to Error */
}
.inProgress {
composes: common;
/* anything that only applies to In Progress */
}
The composes
keyword says that .normal
includes all the styles from .common
, much like the @extends
keyword in Sass. But while Sass rewrites your CSS selectors to make that happen, CSS Modules changes which classes are exported to JavaScript.
In Sass
Letโs take our BEM example from above and apply some of Sassโ @extends
:
.Button--common { /* font-sizes, padding, border-radius */ }
.Button--normal {
@extends .Button--common;
/* blue color, light blue background */
}
.Button--error {
@extends .Button--common;
/* red color, light red background */
}
This compiles to this CSS:
.Button--common, .Button--normal, .Button--error {
/* font-sizes, padding, border-radius */
}
.Button--normal {
/* blue color, light blue background */
}
.Button--error {
/* red color, light red background */
}
You can then just use one class in your markup <button class="Button--error">
and get the common & specific styles you want. Itโs a really powerful concept, but the implementation has some edge cases & pitfalls that you should be aware of. A great summary of those issues and links to further reading is available here, thanks to Hugo Giraudel.
With CSS Modules
The composes
keyword is conceptually similar to @extends
but works differently. To demonstrate, letโs look at an example:
.common { /* font-sizes, padding, border-radius */ }
.normal { composes: common; /* blue color, light blue background */ }
.error { composes: common; /* red color, light red background */ }
That gets compiled and ends up looking like this by the time it reaches the browser:
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 { /* blue color, light blue background */ }
.components_submit_button__error__1638bcd { /* red color, light red background */ }
In your JS code, import styles from "./submit-button.css"
returns:
styles: {
common: "components_submit_button__common__abc5436",
normal: "components_submit_button__common__abc5436 components_submit_button__normal__def6547",
error: "components_submit_button__common__abc5436 components_submit_button__error__1638bcd"
}
So we can still just use styles.normal
or styles.error
in our code but we get multiple class rendered into the DOM.
<button class="components_submit_button__common__abc5436
components_submit_button__normal__def6547">
Submit
</button>
This is the power of composes
, you can combine multiple separate groups of styles without changing your markup or rewriting your CSS selectors ๐
Step 3. Sharing between files
Working with Sass or LESS, each file that you @import
gets processed in the same global workspace. Itโs how you can define variables or mixins in one file and use them in all your component files. Itโs useful, but as soon as your variable names threaten to clash with each other (since itโs another global namespace), you inevitably refactor out a variables.scss
or settings.scss
, and you lose visibility into which components depend on which variables. And your settings file becomes unwieldy.
There are better methodologies (in fact Ben Smithettโs post about using Sass & Webpack together was a direct influence on the CSS Modules project and I encourage you to read it) but youโre still constrained by the global nature of Sass.
CSS Modules runs on a single file at a time, so thereโs no global context to pollute. And like in JavaScript where we can import
or require
our dependencies, CSS Modules lets us compose
from another file:
/* colors.css */
.primary {
color: #720;
}
.secondary {
color: #777;
}
/* other helper classes... */
/* submit-button.css */
.common { /* font-sizes, padding, border-radius */ }
.normal {
composes: common;
composes: primary from "../shared/colors.css";
}
Using composition, we are able to reach into a totally general file like colors.css
and reference the one class we want using its local name. And since composition changes which classes get exported, not the CSS itself, the composes
statements themselves get deleted from the CSS before it reaches the browser:
/* colors.css */
.shared_colors__primary__fca929 {
color: #720;
}
.shared_colors__secondary__acf292 {
color: #777;
}
/* submit-button.css */
.components_submit_button__common__abc5436 { /* font-sizes, padding, border-radius */ }
.components_submit_button__normal__def6547 {}
<button class="shared_colors__primary__fca929
components_submit_button__common__abc5436
components_submit_button__normal__def6547">
Submit
</button>
In fact, by the time it reaches the browser, our local name โnormalโ has no styles of its own. This is a good thing! It means we were able to add a new locally-meaningful object (an entity called โnormalโ) without adding a single new line of CSS. The more we can do this, the fewer visual inconsistencies that will creep into our site and the less bloat weโll be shipping to our customersโ browsers.
Aside: These empty classes can easily be detected and removed by something like csso
Step 4. Single responsibility modules
Composition is powerful because it lets you describe what an element is, not what styles make it up. Itโs a different way of mapping conceptual entities (elements) to styling entities (rules). Letโs take a look at a simple example in plain-old-CSS:
.some_element {
font-size: 1.5rem;
color: rgba(0,0,0,0);
padding: 0.5rem;
box-shadow: 0 0 4px -2px;
}
This element, these styles. Simple. However, thereโs a problem: the colour, font-size, box-shadow, the padding โ everything is specified here in full detail despite the fact that we probably want to reuse these styles elsewhere. Letโs refactor it using Sass:
$large-font-size: 1.5rem;
$dark-text: rgba(0,0,0,0);
$padding-normal: 0.5rem;
@mixin subtle-shadow {
box-shadow: 0 0 4px -2px;
}
.some_element {
@include subtle-shadow;
font-size: $large-font-size;
color: $dark-text;
padding: $padding-normal;
}
This is an improvement, but weโve only extracted half of most of the lines. The fact that $large-font-size
is for typography and $padding-normal
is for layout is merely expressed by the name, not enforced anywhere. When the value of a declaration like box-shadow
doesnโt lend itself to being a variable, we have to use a @mixin
or @extends
.
With CSS Modules
By using composition, we can declare our component in terms of reusable parts:
.element {
composes: large from "./typography.css";
composes: dark-text from "./colors.css";
composes: padding-all-medium from "./layout.css";
composes: subtle-shadow from "./effect.css";
}
The format naturally lends itself to having lots of single-purpose files, using the file system to delineate styles of different purposes rather than namespacing. And if you want to compose multiple classes from a single file, thereโs a short hand for that:
/* this short hand: */
.element {
composes: padding-large margin-small from "./layout.css";
}
/* is equivalent to: */
.element {
composes: padding-large from "./layout.css";
composes: margin-small from "./layout.css";
}
This opens up the possibility of using extremely granular classes to give aliases for every visual trait your site uses:
.article {
composes: flex vertical centered from "./layout.css";
}
.masthead {
composes: serif bold 48pt centered from "./typography.css";
composes: paragraph-margin-below from "./layout.css";
}
.body {
composes: max720 paragraph-margin-below from "layout.css";
composes: sans light paragraph-line-height from "./typography.css";
}
This is a technique Iโm really interested in exploring further. In my mind, it combines some of the best aspects of atomic CSS techniques like Tachyons, the readability of something like Semantic UI with true, dependable isolation.
But weโre only at the beginning of the CSS Modules story. Weโd love for you to try it on your current next project and help us shape its future.
Get started!
With CSS Modules, we hope that weโre able to help you and your team maintain as much of your current knowledge of CSS and your product, but become vastly more comfortable and more productive. Weโve kept the syntax additions to a minimum and tried to ensure there are examples that are close to the way youโre already working. We have demo projects for Webpack, JSPM and Browserify if youโre using one of those, and weโre always on the look-out for new environments where CSS Modules would work: support for server-side NodeJS is happening and Rails is on the horizon.
But to make things even easier, Iโve made a little Plunkr for you to play around with an example without installing a thing! Give it a go:
When youโre ready, take a look at the main CSS Modules repo and if you have a question, please raise an issue to kick off a discussion. The CSS Modules team is small and we havenโt seen every use-case yet, so weโd love to hear from you.