DEV Community

Cover image for Why Your CSS is Always Messy and Chaotic – Understanding CSS Complexity
Shimin Zhang
Shimin Zhang

Posted on • Updated on • Originally published at blog.shimin.io

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

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.cssform.csswidget.csslogin.csscategory.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 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.csscomments.cssglobal.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>
Enter fullscreen mode Exit fullscreen mode

and the CSS files:

/* widget.css */
.widget .widget__content {
    color: red;
}

/* media.css */
.media .media__label {
    color: blue;
}
Enter fullscreen mode Exit fullscreen mode

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 colorfont-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>
Enter fullscreen mode Exit fullscreen mode
.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'  
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
alohci profile image
Nicholas Stimpson • Edited

... you are tasked with changing all paragraph text colors on a website from black to navyOn a large project, you spend the whole day defeating the highest specificity that applies to all <p> elements on all pages.

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.

Collapse
 
shikkaba profile image
Me • Edited

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>

Collapse
 
laurilllll profile image
Lauri Lännenmäki

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!

Collapse
 
jonaspetri profile image
Jonas Petri

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.

Collapse
 
redrogue12 profile image
Edgar X. González Cortés

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.

Collapse
 
hjrobinson profile image
hjrobinson

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.

Collapse
 
miguelmj profile image
MiguelMJ

Great article!