Does the thought of making a minor style update in a large front-end project give you pause? It does for me. It can be especially hard when I'm modifying a project written by another team, even if the change is small.
Somehow, CSS stylesheets (preprocessed or otherwise) are always a jumbled mess of classes and ids. Why is it common to have clean JS/Python/PHP code and spaghetti CSS in the same code base?
This week, I want to take a look at what makes CSS complex, using the ontology of software complexity from John Ousterhout's excellent book A Philosophy of Software Design. This post sets the table for why CSS gets so messy – before we dig into design patterns and solutions next week.
What is Complexity?
Let's start off with a complexity definition. From A Philosophy of Software Design: "Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.".
This definition fits CSS quite well. Complex CSS is hard to understand and harder to modify – obligatory link to the CSS stool joke.
Ousterhout also points out three symptoms of complexity that make a piece of code hard to work with:
Change Amplification
Change amplification occurs when a seemingly simple change requires changes in multiple places.
Example in CSS land: you are tasked with changing all paragraph text colors on a website from black to navy. With a simple HTML page, it's as simple as a one-line p { color: navy; }
. On a large project, you spend the whole day defeating the highest specificity that applies to all <p>
elements on all pages – only to discover the checkout gadget's shipping confirmation modal has a paragraph element that somehow inherited text color from the teal heading.
Cognitive Load
Cognitive load is the amount of information a developer needs to keep in mind in order to make a change.
Example in CSS land: You are asked to change the font-size
of the hero section CTA button on large viewports from 20px to 22px. Here's a list of information you may have to keep in mind:
- Default body
font-size
from user agent stylesheets. - Existing button
font-size
for small and medium viewports. -
font-size
of hero section text because theem
unit seems like a good fit. - Maybe we use
rem
instead, does the project use the standard16px
rem size or10px
rem size for ease of calculation? - Potential hero section variation of the button and its
font-size
. - Any
font-size
utility classes the project may have.
Unknown Unknowns
You face unknown unknowns when it is not obvious where to get the information needed to complete a task, or where to make the necessary change.
Example in CSS land: You are changing the error message color for a <span>
element in a login widget on the category page. Do you make the change in utilities.css
, form.css
, widget.css
, login.css
, category.css
, or index.css
? Depending on your project's CSS methodology, this could be a component variation, a new component, or a new utility class.
Why CSS gets Complex
Next, let's take a look at different technical causes of CSS complexity.
Selector Space
While CSS properties belong to a single matched element at a time, CSS selectors are matched against all elements in the DOM. So, as a project grows in size and the number of total selectors – both actual and potential – grows, we have a natural increase in CSS complexity.
The total number of CSS properties in the DOM grows linearly with DOM size – a page with n
total elements at x
properties per element have at most nx
total adjustable properties.
On the other hand, the size of the set of potential selectors grows exponentially with respect to DOM depth. Assume using only a single class per element, a m
level deep DOM element has 2^m
number of applicable class-based selectors.
Since the total number of potential selectors grows exponentially on large projects, it impacts both cognitive load and change amplification – to understand and modify its implemented subset.
If you ever wondered why front-end developers spend so much time tinkering with and cursing at CSS selectors, it's because the selector solution space for any given change is HUGE.
Specificity
In my experience, there's no quicker road to unmanageable CSS than "specificity escalation". Because any moderately complex project will have a huge set of potential selectors for each change, and because CSS selector specificity score is additive in nature, a project's average CSS selector specificity tends to increase as it grows.
As Chris Coyier pointed out in this answer about CSS code smell, when you see a selector like #articles .comments ul > li > a.button
in your dev tools, it's already too late.
Specificity-related complexity is symptomatic in all 3 categories. We already covered the need to create ever more specific CSS selectors in the change amplifications case – eventually reaching id selectors, !important
, and inline styles.
Specificity escalation also means developers reading and modifying CSS now need to keep a detailed mental model of the DOM in their mind when working – is it .comments
inside #articles
or is it .comments
inside ul
s inside #articles
?
Lastly, ever-increasing selector length means the number of locations that a style change may belong to also increases linearly. In the selector example above, should the change go in articles.css
, comments.css
, global.css
or just use 'find in the project' – and what if you use a preprocessor that supports nested selector?
Source Order
CSS source order gives us a way to consistently predict how a style resolves when multiple selectors have the same specificity score. It also handles when multiple values are declared for the same property in the same scope – an especially tricky combination with CSS shorthands.
The rule is simple, the value that appears last wins.
Straight forward, right? Not so fast. This means each change we make is potentially affected by all other selectors with the same specificity. Your .nav .button
could be affected by .header .button
. In fact, you need to be aware of the intersection of all sets of elements matched by each selector with the same specificity score as the one you are working on. I can't even keep the entire DOM in my head at once, never mind the subset matched by all stylesheets. It's easy to see how this is a significant cognitive load that grows with project size.
Source order is an even bigger problem in large projects where multiple stylesheets are included per page – either plain CSS or via preprocessor imports. Assume you are changing the color of the span in the following BEM-based DOM structure to green:
<div class="widget">
<div class="widget__content">
<div class="media-box">
<span class="media-box__label">Change Me</span>
</div>
</div>
</div>
and the CSS files:
/* widget.css */
.widget .widget__content {
color: red;
}
/* media.css */
.media .media__label {
color: blue;
}
If you have no guarantees on the order of the stylesheet imports, which file do you change? This is a clear case of unknown unknowns, and it grows linearly with project file size.
Do you change both 'just to be safe' and thus engage in change amplification? Or do you write .widget .widget__content .media-box
and engage in specificity escalation?
Inheritance
CSS inheritance governs how certain CSS properties, when not specified, inherit values from their parent elements. Some common properties that inherit include color
, font-size
, and line-height
.
Inheritance is not as problematic as the other issues covered so far. It's a layer of abstraction that has little impact on change amplification and does not produce unknown unknowns. What it does is lead to additional cognitive load, even if the issues are mostly discovered via the browser – can you picture what "hello world" in the below example look like?
<div class="a">
<div class="b">
<div class="c">
<div class="d">
<p>Hello World</p>
</div>
</div>
</div>
</div>
.a {
color: blue;
font-size: 14px;
letter-spacing: 2px;
line-height: 3;
}
.b {
font-size: 12px;
font-weight: 800;
letter-spacing: -1px;
}
.c {
color: teal;
font-weight: 600;
font-family: 'Serif'
}
.d {
font-size: 18px;
font-family: 'arial'
}
Final Word
I hope I've convinced you that CSS is inherently complex, and its complexity grows superlinearly with the size of your project.
Next time you get frustrated with a design change that seems to take much longer than it should, know it's probably caused by the project's CSS complexity.
Next time you see a long selector in a code review – call it out and nip specificity escalation in the bud.
And if you are starting a new project, I hope this post has further convinced you to adopt a tried and true CSS methodology and enforce it – really enforce it. No need to make CSS more complex than it has to be.
Next week, I'll go over some historically successful patterns for combating CSS complexity, their tradeoffs, and the methodologies that use them.
If you found this post helpful, please share it with others. You can also follow me @itstrueintheory or on Twitter @itstrueintheory to get the latest blog updates.
Top comments (7)
No you don't. You spend all day removing all the CSS that applies to all
<p>
elements on all pages, because it's no longer needed. What a great opportunity to clean up lots of styles!Or rather of course, you don't. Because that's a very unlikely scenario. If multiple developers have spend ages setting up all different colours for different paragraphs to meet multiple requirements over an extended period of time, the chances that the app is going to suddenly want the same paragraph colour everywhere is practically nil.
What's far more likely is that you'll be tasked with setting specific paragraphs to a new colour. Which, if you've got your specificity set correctly, should be a trivial matter. Specificity is a wonderful design, if you use it properly. But like writing good, maintainable JS code, it's a skilled job to do it. The real problem is that doesn't get the respect that it deserves.
Which file do you change? Just widget, unless you're going to do add something specific as .media .media__label is NOT in the html.
<div class="media-box">
<span class="media-box__label">Change Me</span>
</div>
Great in depth article!
But rather than telling a story about CSS complexity, this is a story about how CSS is mistreated and about the lack of good practices.
It’s the people who make CSS complex.
Looking forward to your next article. You are a very good writer!
With Tailwind you at least don’t need to be afraid of nested selectors or be confused in which file you should change stuff, that’s a good thing with it.
Hey Shimin, awesome article. I've had problems with CSS complexity before. In a particular React project we had devs copy paste styles from one component stylesheet to another. There was no scoping for the stylesheets so we had styles from one innocent component mess things in other places. Instead of fixing the root issue, more specific selectors were made in the main stylesheet and the problem grew worse.
As you mention, the moment you are forced to rely on using ids and !important you've already lost the battle.
I would try not to be too finicky when when in the development stage. If you're working in React just use styled-components and then trim away the fat later.
Great article!