Writing better CSS with meaningful class selectors
Published 10/03/2015
With the rise of frameworks like React, Angular, Ember and the Web Components spec, the web seems to be moving towards a more components based approach to building apps. The abstractions for these are getting better, except for one part: CSS. In many ways, we write CSS just as we wrote it years ago. Even if we think about our UI as being more modular, the CSS is still spaghetti. Last year I was tasked with designing a CSS system for a components library and after much trial and error I found a quite satisfying pattern, that allowed for the codebase to grow in a quick and maintainable way.
Let’s start by looking at some standard spaghetti. Say we “think” about our CSS classes as “components”.
.menu
.overlay
.button
And we want a “modular” CSS system. So we start writing rules like this:
.menu .overlay > .button
#home .sidebar.managers-special
Stop.
Last time I checked “modular” did not mean cobble everything together with a jackhammer. All our elements are now tied together and the selector makes it ambiguous where it belongs (is it part of the page? Is it then a “Component”?). By side effect, it will also force our HTML to a certain structure as well.
<nav class="menu">
<div class="overlay">
<button class="button">I am button</button>
</div>
</nav>
It's not uncommon to mix "components" as well:
<div class="menu overlay"></div>
The "flexibility" of the language is what makes it so hard to reason about; could we avoid some of it’s features to make it better?
From my experience, the answer is yes.
Classes are the most powerful selectors because we have the most control about what they actually mean. But if that meaning is fuzzy and not respected, we lose its benefit. We need to make sure that the way we write our selectors keeps their meaning and not muddies it.
When working on a pattern library for econsultancy.com I've experimented a lot with approaches to writing such classes, and ended up with 3 rules with help the most:
1. A CSS class always represents a component. Any other abstractions that a CSS class may represent needs a prefix, suffix or what have you - they need to be easily distinguishable and the convention needs to be well established.
Example:
// In this project, components are:
.menu
.menu-title
// Classes that modify a component start with a `-`
.-left-aligned
.-in-sidebar
// Helpers are started with `h-`
.h-margin-bottom
// Theming classes are started with `t-`
.t-brand-color
2. All CSS selectors that contain a CSS component class must start with it and they must not modify any other CSS component class or it’s children directly (by referencing it - so a selector can ever include only a single CSS component class) or indirectly (by element selectors for example).
Example:
// Given this HTML, where components are `main-logo`, `.menu` and `.menu-item`:
<header>
<img src=/images/logo.png class=“main-logo”>
<ul class="menu">
<li class="menu-item">
<img src="/images/avatar.jpg">
<a href=#profile>Go to profile</a>
</li>
<li class="menu-item -last">
<img src="/images/avatar.jpg">
<a href=#profile>Go to profile</a>
</li>
</ul>
</header>
// Ok selectors. Element selectors here would be defaults for given tags.
img
a
header
.main-logo
.menu
.menu-item a
.menu-item img ~ a
.menu-item.-last
// Not ok
header a
header .menu
.menu .menu-item
img ~ a
.menu-item img ~ a
.menu li:nth-child(2) a
3. An HTML tag can only have a single CSS component class.
Example:
// Ok
<button class="action-button">
<section class="panel">
// Not ok
<div class="menu parallax-scroller click-hijacker-visible">
<a class="action-btn trigger-overlay">
<ul class="sub-menu facets">
As with any subset, in some cases this will make writing some of your CSS harder and more verbose. It’s probably an overkill for smallish projects, where what you need a stylesheet and not a component library. It’s probably not well suited to drawing iPhones with box-shadows either.
Things can be done with preprocessors to simplify this approach. For example, on the aforementioned project, we used SCSS and all classes were output with mixins. This made for some crazy looking CSS, but in the end, all developers on the project found it much more easier to extend and reason about our CSS.