I had a question via email a few days back from someone working on a Masters project. The subject being how people write CSS. The person in question had heard me talking as a guest on the ShopTalk Show podcast about not mixing abstraction and isolation when it comes to scaling CSS projects and wondered if I had talked/written about the point more fully.
It’s certainly a theme throughout the entire Enduring CSS book (read it free online if you would rather, or just skip through the slides if you want an overview) but I perhaps haven’t addressed my feelings on the subject specifically. For completeness, this post aims to do that.
However, for the sake of completeness and posterity, I’ll surmise my opinion here.
If you are maintaining a large CSS codebase, whether that be large due to qunatity of code, or large due to quantity of developers, or likely both; you need an approach to writing CSS that scales. By ‘scales’, I mean an approach to authoring styles that facilitates developers working on the CSS code with relative ease. Working with the CSS codebase includes fixing a problem or styling new components without adversely effecting anything unintentionally.
Does your approach to writing CSS allow developers to add to, remove from, and amend your product with entirely predictable results? That is the remit of a CSS approach that can scale.
My belief is that when it comes to scaling CSS there are two approaches that work: complete isolation and complete abstraction. Anything else ultimately becomes sub-optimal.
Isolation in CSS terms is the ability for the code you write to be isolated from anything else, preventing anything intended for one visual entity to ‘leak’ into another. Isolated code is easy to reason about, write and delete because by virtue of it being isolated, it cannot effect anything else. Approaches in the isolation camp are ECSS, BEM, styled-components and CSS Modules.
Abstraction in CSS terms is the apposite approach to isolation. A generally finite set of very generic CSS classes are assembled like lego bricks to achieve the desired effect onto each element. Parts of the abstraction CSS codebase never get deleted over time (as any existing component may depend on any piece of the abstraction toolkit) but tends to stay small over time because anything can be made from the little parts you have at your disposal from the outset. Popular approaches in the abstraction camp are Atomic CSS and Tailwind CSS.
For this post I’m not interested in debating which approach is ‘best’. I’m not a fan of absolutes; there are simply problems and solutions. What is important is that either approach can work. What works for you might not work for another. Choose your poison.
The problems lie in the middle
Imagine isolation and abstraction at either end of a continuum. The further you move away from either end, the more complicated and less effective your approach is likely to be.
Abstraction is fine because you know anything can be made from the existing building blocks and they are never going away. Need a new component? No drama; just write the HTML, bang on the relevant classes and there you go. You aren’t going to change a class as generic as
m-10p to be anything other than
margin-top: 10px so things stay sane and predictable.
Isolation + abstraction
Isolation is fine because you know that the styles you write will only apply to the elements they are targeted at. Things are kept sane in development land because when that components time is up, you just delete everything to do with the component, including the relevant styles and nothing else is effected. Everything you make is ‘green-field’ so you are free to make anything however you like. With isolation, you don’t make things that are easy to extend. You make things that are easy to delete.
However, things generally go bad when you try and mix approaches. Starting with an isolation approach, imagine creating 5 different isolated components.
<div class="my-First_Component"></div> <div class="my-Second_Component"></div> <div class="my-Third_Component"></div> <div class="my-Fourth_Component"></div> <div class="my-Fifth_Component"></div>
However, as you make them you notice that they all share some similarity. Suppose they all currently have the same main font-size, font-weight and colour. Seems like a perfect time to abstract that similarity and DRY up your code? You make another class that can be shared across these components and any other elements that share those similarities.
<div class="my-First_Component hlt"></div> <div class="my-Second_Component hlt"></div> <div class="my-Third_Component hlt"></div> <div class="my-Fourth_Component hlt"></div> <div class="my-Fifth_Component hlt"></div>
We’ve added a utility style here,
hlt for ‘HeadLine Text’ and that goes into a ‘global.css’ or ‘utility.css’ stylesheet.
Fast forward 6 months and a relatively new dev to the company is on call and gets an urgent ticket.
my-Third_Component needs the main text tweaking as the text is too long and it’s obfuscating some important T&Cs information. He inspects the code, find the class in question, makes the tweak, commits the code and gets back to bed. He wasn’t aware that he had inadvertently changed 4 other components.
There are umpteen variations on this scenario. None of them end well. Perhaps instead of a call-out scenario it’s a code removal situation. Those 5 components get removed from the code base, but because
hlt is generic and not encapsulated with the component, it then lives on somewhere else, with no-one ever confident as to its usage or whether it too can be removed. By mixing approaches we’ve failed to meet our original remit: we are no longer able to add, remove from or amend the product with entirely predictable results.
Abstraction + isolation
Let’s flip it and look at abstraction.
Suppose the components were made with an abstraction approach. Everything is going well in the codebase until someone decides that they can save time by adding a single class that does a bunch of things in one go. Perhaps they feel adding individual classes to each node is simply laborious. They make the class
m10-p10-b3-green-fLarge. It adds 10px margin, 10px padding, a 3px border in green and a large font. They add this one class to the components they are currently making to speed up development. However, when they need some future variation of the thing they are making, or to adjust it slightly (maybe they need one with only 5px of padding), they can’t alter that original class. So they either add an ‘undo’ utility class that overwrites the original 10px of padding or they remove that
m10-p10-b3-green-fLarge class from the component and remake with separate utility classes and the that prior class hangs around, potentially for perpetuity.
Again, there are multiple possibilities for things to come undone when you ‘cross the streams’.
Now, none of this takes into account the great tooling and solutions that sit around either isolation or abstraction approaches. It does however help illustrate that the each approach is fundemantally strongest when they remain true to their raison d’être.
My preference is always simplicity. The more rules and caveats an approach has, the more difficult it is to communicate concisely to people. The harder to communicate an idea easily, the more brittle and open to interpretation it is. This includes approaches to writing CSS.
Whenever I encounter approaches that require developers to think about whether or not a class they are making is a utility class, a base class, a decoration class or (insert your own other nebulus characteristic) I always feel they are needlessly-complicated and therefore prone to failure.
Developers that touch the CSS may not be as versed in CSS as others. The appraoch should be as easy for them to deal with as possible. If not you have needlessly created a gatekeeping situation.
Either embrace isolation and deeply understand why it works or embrace abstraction and deeply understand why it works. Whichever you choose, it you embrace WHY they work and enforce that approach you should enjoy a CSS codebase that can scale to any need.