Good Fences: Neighborly Styling with CSS Modules

Posted by Dan Freeman

Feb 24, 2016 9:57:55 AM

fenceHave you ever noticed that no one writes "How we name our Ruby variables at Company X" blog posts? No one's making the Hacker News front page with "I combined these two strategies for method naming and suddenly my JavaScript is maintainable!" And yet when it comes to CSS, developers are all about naming strategies. We sing the praises of BEM, SMACSS, OOCSS, SUIT, or whatever other set of capital letters is popular this week. Why is that?

Like the rest of the Web as a platform, CSS has been press-ganged into doing far more than just presenting the static documents it was originally designed for. And also like the rest of the Web, CSS has grown to meet many of the new demands we've made of it, with new capabilities like media queries, animations and flexbox that make the language itself more powerful, and processors like LessSass and PostCSS to make authoring complex styles easier. Since its inception, though, one fundamental piece of the language hasn't changed, and it's the source of our obsession with naming: CSS has a scope problem1. If I create a class called large, I have to be sure no other CSS in my app or its dependencies is using large to mean something else. And later, if I want to change or remove my large class, I have to comb through my markup to make sure I understand all the places it's currently being used.

Then and Now

In 1996, for a single author creating a single document, CSS represented a huge step forward. By allowing authors to take all their styling out of the content itself and put it in a single stylesheet, both the content and the styling became more readable, and sharing consistent visual treatments in different parts of the document became possible. You could even use styles someone else had written, as long as you made sure they didn't conflict with any of your own.

Fast forward twenty years, though, and we're no longer single authors writing single documents. We're large groups of people writing enormous applications, and now we aren't just contending with class names from bits of CSS we pasted off of some forum, but from dozens of libraries we're pulling in from Bower or npm, not to mention our teammates and past selves working in other parts of the application. Despite this, our approach to managing the styles from all these disparate sources still essentially amounts to "throw it all in one big file and hope nothing clashes."

What's in a Name?

If I want to make sure my idea of large doesn't conflict with the idea of large from that shiny new widget I just installed, there are any number of na├»ve selector-nesting approaches I might try (like .my-container-class .large), but at scale those solutions are refactoring and performance hazards, and they don't help if the opposing large definition isn't wrapped within its own container class. This is where our alphabet soup of naming methodologies comes into play.

Rather than try to coerce built-in mechanics of CSS to scope our selectors, we take the burden on ourselves as developers and make sure the names never overlap in the first place. We've invented so many elaborate different ways of pulling this off, and they bring real benefits. When you know your class names are context independent and uniquely named, it's trivial to figure out exactly where they're being used, and refactoring becomes much less scary.

However, these naming methodologies also add cognitive overhead, they clutter both the markup and the stylesheets, and ultimately they rely on human beings doing the right thing, so they're still error-prone. Even so, we cling to the hope that the next__big--thing, or maybe the Perfect Combination-of-methodologies, will be the silver bullet that makes everything easy and simple. And yet. And yet...

I can pull up any half-baked jQuery plugin on GitHub that has 10 different for loops that all call their iteration variable i, and yet they don't conflict with each other, and vitally, they don't keep me from having my own variables that are also unhelpfully named i. As in most full-fledged languages, JavaScript variables are local to the area they're defined in2, so there's no danger of them leaking out, and they only need to be named specifically enough to make sense in their own particular context.

Why Can't CSS be Like That?

Thanks to the CSS Modules project, it can! The everything-is-global burden moves back off of the developer and on to the tooling, effectively making each CSS file its own isolated namespace by transforming class names to ensure they're unique. This aligns nicely with component-oriented UI frameworks like Angular, React and Ember, where many of the styles you want to apply are specialized to a particular component.

What about shared styles, though? One of the most valuable things CSS provides over inline styling is a way to define a style once and apply it in different places, and it would be counterproductive to completely lose that. For such cases, we build off of patterns established by the ES6 module system for explicitly opting into sharing by importing names from other modules.

For instance, I might put my large class from before in a typography module, define some color constants in a colors module, and then use those to define the styles for a header in a particular component:

/* components/my-component.css */
@value primary-color, secondary-color from '../colors.css';
.header {   composes: large from '../typography.css';   color: primary-color;   border-bottom: 1px solid secondary-color; }

My header class is constrained to the context of the my-component module, and I explicitly import it into scope anywhere I want to use it in CSS or JS. Because of this, it's possible to statically determine where each class is used, enabling things like dead code elimination and dread-free refactoring.

The mapping from the original class names to the guaranteed-unique ones is available as a JS object for each module. From there, applying those classes to your markup comes down to the details of your UI framework of choice.

Modular CSS for Ambitious Applications

At Salsify, we're excited by the promise of the CSS Modules project, and we're eager to explore the patterns it enables and the possibilities it opens up. We're also an Ember.js shop, so naturally our first move was to build an addon, creatively named ember-css-modules. The addon enables Ember-flavored use of CSS modules, exposing the classes for a module in a styles property on the corresponding component or controller:

{{!-- templates/components/my-component.hbs --}}
<div class="{{styles.header}}">Title</div>

The addon is still an ongoing project, but it's already completely functional, so give it a try! We'd love to hear your thoughts.

Further Reading


1. Perhaps more fairly, CSS has a single global scope, and at scale, that's a problem.
2. Where "area" is a vaguely defined term that might mean function, block or something else entirely depending on a bunch of different factors
comments powered by Disqus