Structuring CSS in large projects
Writing CSS is easy.
Writing maintainable CSS is hard.
You’ve probably heard this a 100 times before.
The reason is that in CSS everything is global by default. If you’re a C programmer you know that global variables are bad. If you’re any kind of programmer you know that isolation and composable modules are key in building maintainable systems.
So, what’s the issue with CSS?
With a CSS definition like the one above, styles immediately become global, and affect any and all pages your styles are being applied to. There is no encapsulation. There are no isolated modules.
In a standard programming language you import only the modules you need for the particular functionality you’re implementing, e.g.:
# Python modules
from Flask import url_for
// Node modules
var express = require(‘express’)
This way, you know exactly what’s going to affect your code, and only stuff you have explicitly imported have influence on the functionality that you’re implementing right now.
In CSS the roles are reversed. Every time I write a line of CSS, I could potentially be affecting everything else in the project, and inadvertently changing the look of other pages than the one I’m working on right now. My styles are not just leaking; they’re flooding out and spilling into every nook and cranny of my application.
Now this is understandable, and makes sense for basic styling like typography, simple styling of input fields, and styles that are inherently global. This is basically what HTML and CSS was built for. These tools were built for publication. To understand the thought behind these languages, I often imagine typesetting a book: You don’t want every page to look different — no, you want a simple coherent style throughout the book without much riff-raff. That’s why it makes sense to have tags like <h1–6>, <section>s, and styling that is global and ever-present.
However, the world has changed, and the web has changed. We no longer build web pages — we build web applications. The publication metaphor that HTML and CSS was built around, no longer applies to most things built on the web today.
Really this calls for a new way of specifying styles, and perhaps a new way of building for the web. But, for now, we’re stuck with CSS and HTML, and that means we have to use these tools carefully, in a way that produces manageable and maintainable web applications.
The way Peergrade.io does CSS
1st rule: Use a prefix (for class names)
At Peergrade.io we use the prefix .pg in all class names. Not using prefixes in your CSS codebase is asking for trouble. The reason being that non-prefixed class names will eventually clash with imported styles. Say you need a datepicker — you definitely don’t want to build it from scratch (at least I hope not!) So you import it. Now you’ve got classnames like .prev, .next, and .separator flying around in your styling, potentially clashing with your classnames — if you haven’t used a prefix.
For the longest time Font Awesome didn’t use prefixes in their classnames, which meant that you’d often run into nameclashes with their .icon-* classnames (they now use the prefix .fa). We’re also a bit sad that Bootstrap has chosen not to prefix — but we still ❤ you, Mark Otto.
2nd rule: No CSS selector nesting
At Peergrade.io we use Sass. Using Sass you quickly get into a pattern where your Sass structure matches the structure of your HTML, e.g.:
After a while of doing this– you realize that, while it feels nice — it’s very brittle. When you write it, you may think that there will only ever be one list in the .profile-description, but after a month or two you figure out there has to be another list in there and the structure quickly outruns your assumptions.
Also, style definitions like these will apply to any element inside of the parent element–not just at the level you put it here in the Sass.
3rd rule: Build components with BEM naming
As far as possible try to build isolatable components, with class names following the BEM-naming scheme. We don’t follow the full BEM guide — just the naming scheme, meaning class names should be of the form
To achieve this we structure our Sass like this:
// becomes `.pg-deadline__date`
// becomes `.pg-deadline__header`
// becomes `.pg-deadline__header--highlight`
What you can see here is that we use Sass nesting to create the BEM classnames for us. Somewhat counterintuitively, this will actually produce a totally flat CSS structure — with no nesting — only top level classname definitions.
As an exception to rule #2 we also allow for classnames of the form .block--modifier
In this special case we allow for 1 level of CSS selector nesting. This allows us to only specify the modifier on the block — which is what’s being modified— and not having to repeat the modifier on all of the children of the block (the elements or “E” in BEM).
To get a better understanding of this BEM-like naming scheme head over to the BEM-like naming part of Harry Roberts’ CSS Guidelines. (It should be mentioned that we only found out that Harry had actually built a naming scheme similar to ours after-the-fact)
It seems that no one has really found the way to do CSS, and looking at this recent article featured on Hacker News, I’m somewhat disappointed with the CSS we ended up with, compared to what could have been.
The bottom line is: we believe we have found a sustainable basis for our CSS — with room for improvement, of course. The plan is to check with our guidelines every so often, to see if things are working the way we expect them to, and revise if necessary.
Our favorite CSS references
- Harry Roberts’ CSS Guidelines
- The CSS Wizardry blog (also by Harry Roberts)
- The Maintainable CSS framework