Why Your CSS is Always Messy and Chaotic – Understanding CSS Complexity

This is part one of a multi-part series that explores maintainable CSS for large projects, you can find the next part here.

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 the em unit seems like a good fit.
  • Maybe we use rem instead, does the project use the standard 16px rem size or 10px 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.

Take a look at this simple DOM example:

<div class="side-bar">
    <div class="widget">
        <div class="widget__content">
            <div class="media-box">
                <span class="media-box__label">Change Me</span>
            </div>
        </div>           
    </div>
</div>

Here's the list of 15 class-based selectors you can use to modify the label style :

.media-box__label
.side-bar .media-box__label
.widget .media-box__label 
.widget__content .media-box__label
.media-box .media-box__label
.side-bar .widget .media-box__label
.side-bar .widget__content .media-box__label
.side-bar .media-box .media-box__label
.widget .widget__content .media-box__label
.widget__content .media-box .media-box__label
.side-bar .widget .widget__content .media-box__label
.side-bar .widget__content .media-box .media-box__label
.widget .widget__content .media-box .media-box__label
.side-bar .widget .widget__content .media-box .media-box__label

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 uls 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. Here's a link on 4 techniques to manage CSS complexity.

If you found this post helpful, please share it with others. You may also be interested in learning about How CSS Custom Properties can be used to Create a Styling API or What's the Big Deal with Tailwind CSS.

Follow me on Twitter @ItsTrueInTheory to get the latest blog updates or subscribe to email updates using the form below.