CSS Architecture is Really, Really Hard

by Drew Barontini

I’ve come to the conclusion that CSS architecture is the most difficult thing I spend my time on. I’ve built a Sinatra application powered by Basecamp’s API, spent time working with the React JavaScript framework, built a few Rails applications, and started learning more Ruby (outside of Rails). Yet, for me, CSS architecture takes the cake for difficulty. And why is that?

  • A multitude of ways to solve the problem
  • Determining how abstract is too abstract
  • Constant second-guessing
  • Dealing with the CSS cascade
  • Naming is hard
  • Knowing the “right” path isn’t easy

A multitude of ways to solve the problem

I know, I know — this is true for any and all programming or coding languages. But, dealing with architectural decisions in CSS yields different problems.

Is this a new element, a modification to an existing element, or should we refactor an existing element?

If I could boil down the constant, internal debate into a question, this would be it. The most difficult part for me is that we can make the argument for any of the above paths.

Is this a new element?

Is it clear that we need a completely new element to house the styling for the pattern? This is usually the easiest of the questions to answer. We can generally discern no other module has the appropriate styling for this new pattern, so we will create and name a new module. However, the other two questions can be murkier.

Is this a modification to an existing element?

The example I see a lot is lists. Several common patterns we use can be a “list”, but if we let the module grow too large, so does the codebase’s complexity. For example, we can limit the scope and responsibility of the list module.

.list { }
.list--inline { }
.list--object { }
.list--styled { }
.list--styled--numbered { }

I’ve had success with this limited set of classes (and related styling) for lists.

A good example of over-extending the list module is the breadcrumb pattern. We should readdress the questions we talked about earlier and determine the appropriate path. In my opinion, breadcrumb becomes its own module. Why? Its styling, although like an inline list, would be better suited as a new module. Even consisting of a small set of styles, it adds to the scope and responsibility of the list module.

This depends on the particular styling of a breadcrumb navigation list, but the styles are generally unique enough.

With our separate breadcrumb module, we can use it to extend styling of an inline list, like so:

<ol class='list list--inline breadcrumb'>
  <!-- ... -->
</ol>

We’ve now kept the list module’s responsibility minimized, avoiding adding more scope and responsibility.

I have an article about the Single Responsibility Principle that you can read more about, if you’re interested.

Should an existing element be refactored?

But what happens if it’s clear that an existing element (or module) can be refactored to accommodate the styling for this new element?

  • How do we know it’s time to refactor?
  • How do we know the best way to refactor?

For me, this is the most difficult problem. Refactoring alone isn’t easy, but determining when and how is that much more difficult.

Determining when to refactor

There are no hard and fast rules for knowing when to refactor. Generally, we look for overly complex modules, as well as overly specific modules. If we’re feeling the pain when building out new designs, it’s time to look at refactoring.

  • Is the module doing too much?
  • Are there a lot of modifiers (for the variants of the module)?
  • Are any of those modifiers resetting styles on the base module?
  • Is there styling that can be decoupled into a higher-level module?

There are so many questions that make our jobs difficult. Once we’ve determined that it’s time to refactor, we actually have to do it. Oh, joy!

Determining how to refactor

So what is the best approach for refactoring? My most successful approach is to do it in the smallest set of changes possible. CSS doesn’t have the luxury of tests to verify equal functionality. We have to make each change and test ourselves. And this doesn’t include cross-browser-and-device compatibility, accessibility, and affected performance.

So, using an example, let’s assume the following determination for refactoring:

We need a new module with a more abstract name, and we need to split one set of the modifiers into its own, newly created module.

We might take the following steps (considering each step to be a single Git commit):

  1. Rename the file and the classes in the HTML and CSS
  2. Create a new module for the modifier
  3. Rename the old module classes in the HTML and CSS to the new module
  4. Test that the styles behave as expected

In a large codebase, it’s important to make incremental changes when refactoring. Doing too much at once will cause headaches, making it harder to test that the changes work as intended.

How abstract is too abstract?

Another difficult component to CSS architecture is determining when we’ve gone too far. Is there a point where our application, or individual components, have too much abstraction. And why might that be a problem?

With abstract modules, we can blur the lines of style overlap, increasing codebase complexity. It’s vital to know each module’s responsibility, as well as how they combine to form design patterns. For example:

<div class='cell well'>
  <!-- ... -->
</div>

The cell and well modules each have their own responsibility.

  • The cell is responsible for width-limiting
  • The well is responsible for vertical spacing

We can combine these classes in the HTML to implement a design pattern. They are abstract, but not so abstract as to make it unclear how they work together. This is a constant trial-and-error process when building out a CSS architecture. We have to look at every pattern and find the abstract ones and the specific ones. From there, we’re deciding:

  • Is this a base-tag styling?
  • Is this an abstract module that will be applied to multiple elements?
  • Is this a specific module for this one particular pattern?
  • Is this any combination of the above?

Constant second-guessing

Another struggle I have with CSS architecture is constant second-guessing.

Do I have any clue what I’m doing? Does this codebase make any sense? Am I over-complicating the styles? Am I trying to be too clever? Should I rewrite everything?

I have these moments whenever working on CSS architecture. The difficulty at Code School (and any product application) is living codebases. Our CSS architecture is an iterative refinement process. We’re always building new features, encountering new design patterns, abstracting, and refactoring. It makes second-guessing ourselves just part of the process.

The curse of the CSS cascade

Ah, the “C” in “CSS.” We love it. We hate it. Without it, we’d be duplicating so many styles. With it, we’re battling specificity, having to overwrite on the module level to get what we want.

Whether we decide to style base tags, they are always inherited at the browser level. So, no matter what, we’re dealing with specificity. The struggle is knowing how much style to give to base tags, and how much to leave at the module level.

As with everything in life, “everything in moderation” applies here, too. Apply only the styles that you can reasonably guarantee as the most common occurrence of a base tag. If you find yourself overwriting base tag styles at the module level, then refactor. If you find yourself adding the same style to a base tag at the module level, then abstract.

h2 {
  margin-bottom: 10px;
  /* ... */
}

.card-title {
  margin-bottom: 0;
}

.course-title {
  margin-bottom: 0;
}

With our margin-bottom on the base h2 overwritten so much at the module level, we should refactor. It’s clear the base h2 styles are not the most common occurrence of the tag. But, with anything CSS architecture related, we have so many options:

  • We could create a new heading module with a heading--space modifier to apply the margin-bottom of 10px
  • We could create a mbm utility class (“margin bottom medium”) to only add margin-bottom: 10px to the necessary h2s

There’s an argument for either solution, and for more solutions that aren’t mentioned here. Our job is to make the best decision for our architecture. Decide with the team (those authoring HTML/CSS) to make the best decision.

Naming is hard

Ah, naming. Again, this isn’t a problem specific to only CSS. However, CSS has the semantic-naming mechanism built into it. Our class names need to make sense, but also be abstract enough that they can work with a wide range of elements.

I find myself typing a word into the Thesaurus to look for synonyms. Sometimes I try and get too clever, sometimes too simple. The struggle is real.

Knowing the “right” path isn’t easy

All in all, knowing the ”right” path is tough. There are so many roads, paths, and unexplored areas for “proper” CSS architecture. Not only is the subject difficult to perform, writing about it is just as hard. I hope that this doesn’t come across as incoherent babble from a deranged lunatic. Writing modular CSS can sometimes feel like incoherent babble, so perhaps I’ve done my job.