How to write maintainable CSS

CSS has a maintenance problem and preprocessors or current CSS architectures don’t help solving it. Instead they reinforce the language’s weaknesses and add more problems. But we can use components to do better.

Change is certain

One of the few things in software that is constant is change. Even more so when the software describes the visual presentation of a product. It’s other people’s job to change the design and therefore code: designers, product people, marketing departments, copywriters etc.

Besides the inherent nature of the problem, there is also the team of developers that changes. New people are hired, some leave and they all have different levels of experience and expertise in writing CSS.

Changing CSS is hard

The good news is CSS makes it very easy to change anything. The bad news is CSS makes it very easy to change anything.

Unintentional changes is what makes updating CSS hard.

This problem exists by design: CSS lacks encapsulation. Declarations are global. Source code order matters. The cascade and specificity can override styles. HTML tags have different default styles and these differ across browsers.

Unintended changes might come from:

  • some other code using the same selector
  • changed order of source code
  • some parent element overwriting styles
  • default styles from an unintended HTML tag
  • varying default browser styles
  • unsupported features

Writing maintainable CSS means reducing the number of possible error sources.

Preprocessors make change harder

Unfortunately preprocessors don’t do anything to reduce the list of possible error sources. Quite the opposite, they add more global state, complexity and inheritance to the mix.

Easier nesting encourages adding specificity.

Variables in most preprocessors are global, mutable and depend on the order of source code. This adds another source of unpredictability. (Few variables are still helpful for things like a consistent colour scheme or font family.)

Mixins take the previously easily readable declarations of an element and scatter them across multiple files. Mixins with arguments add more state.

Extend adds a hierarchy of inherited declarations that tightly couple elements and make it hard to decide where declarations should be added or changed. (CSS modules wrongly calls its inheritance mechanism composes)

The way preprocessors add complexity, leads to code that requires more knowledge to make a change.

Preprocessors provide wrong abstractions to the problem of creating reusable visual elements on the web. This leads to highly coupled code, that is hard to change.

Example code that’s hard to change

Imagine you are new to a project or haven’t worked on it for a while and you need to change the design of a button. This is what the class in question looks like:

.button--small {
  @extend %button;
  @include center;
  font-size: $text-small;
  border-radius: $default-radius;
}

Depending on what you need to change and how complex the inheritance hierarchy and mixins are, it might take a lot of time just to figure out what the current declarations for this element are. You need to find where %button, center, $text-small and $default-radius are defined and understand how they work. Keep in mind that each of these classes and mixins might again extend and include other files.

The introduction of dependencies to CSS is the problem. Because each of them could be used by any selector, it’s hard and sometimes impossible to know if a change to this class or its dependencies won’t introduce unintentional updates.

Making a change to .button--small or any of its dependencies might have unintended changes to elements that:

  • depend on %button
  • depend on center
  • depend on $text-small
  • depend on $default-radius
  • extend .button--small

Using features of a preprocessor has made this class hard to change, because it could now be coupled to any number of unrelated elements. I have seen classes that had much more dependencies and were therefore more coupled than this one. Unfortunately from my experience, this is what most CSS code looks like nowadays.

Creating a new class is not a solution either, as it might cause unintentional changes for the next developer. For example he/she might intend to update all buttons, but then yours did not change, because it got decoupled from the dependencies or important styles were overwritten.

Preprocessors allow creating a tightly coupled hierarchy of selectors, that is separate from the DOM.

Visual regression testing is also no solution to this. Solving a problem we created ourselves with even more brittle tooling makes things worse.

The solution is building components


With component I mean a visual element, which has these properties:

  1. It’s a combination of HTML and CSS (and optional JS).
  2. It can be composed of other components.
  3. All styles are written to only affect the HTML of its component.
  4. All styles are isolated. (never overwritten or used by other components/elements)

There are multiple ways to achieve these properties, some involve human discipline and some using current tools properly.

How to create components

Regarding the first and second point, the most popular way to create components these days is React, but you can use any other virtual dom library or even a simple function that gets state and returns HTML.

It doesn’t matter if components are rendered on the server or in the browser. I recommend having a folder for each component so that HTML, CSS and JS are close to each other in the file system.

There are a lot of good posts, that explain how components and their composition work. Let’s come back to CSS and the remaining properties of a component though.

Isolating styles

To only style HTML elements that are part of the component one can use a unique class for each styled element in the component. Tag selectors can easily affect a child component’s elements, especially when nesting components.

Only using classes makes isolating styles across the whole project easier too:

The manual solution is to give each component a unique name and prefix all classes used in this component with it. I recommend naming each file like the component. This has two benefits:

  • Quickly find components by their name.
  • It makes it easy to write a script that traverses all .css files searching for the ones that have classes not prefixed with the component’s name. These are bad components that break the system.

An automated solution is using CSS modules. Apart from composes it’s a great solution to encapsulate styles. Each class is automatically suffixed with a unique hash to prevent naming clashes.

How components prevent unintended changes

Let’s recap where unintentional styles might come from:

  • some other code using the same selector
  • order of source code
  • from a parent DOM element
  • default HTML tag styles
  • default browser styles
  • browser support

With components the first four potential sources of errors are gone:

  • Exclusively using classes and giving them some kind of unique prefix/suffix avoids unintentional changes to other elements.
  • Unique class names make the order of source code irrelevant.
  • Not using tag selectors and using unique component prefixes and classes removes the problem of parent elements unintentionally overriding child elements.
  • As components are a combination of HTML and CSS, styles are always used with the intended HTML.

The last two points are not directly solved by components, but they isolate potential problems. To get more consistent default browser styles I recommend normalize.css. Browser support will always be an issue, but browsers ignore unknown declarations and render the rest just fine. Use this to your advantage and read about progressive enhancement if you don’t know what it is.

Example code that’s easy to change

Here’s how the styles for the button might look like when using a component architecture:

.button {
  display: inline-block;
  padding: 1em 2em;
  background-color: blue;
  text-align: center;
  border-radius: 3px;
}

.button--small {
  font-size: 0.8em;
}

If you follow my advice, they live in a file called button.css. The most obvious improvement is that all styles are at one place, because we don’t use a preprocessor anymore. This saves a lot of time and cognitive energy.

With a component architecture you stop searching declarations across multiple files and keeping them in your head.

It has a much more important advantage though:

Making a change to this class won’t unintentionally affect any other element.

All changes are localised to this component. The chance of bugs has become much smaller.

You can easily add a new modifier class, if a component needs to look differently for certain states. If styles or functionality diverge too much, create a new component. This makes it obvious for the next developer, that it is a different visual element.

Creating components has another big benefit: it’s easy to search for unused components and therefore easy to delete unused code. If there is no import statement for a specific component, it’s safe to delete it.


A component architecture scales very well

Components are not new. It’s just an application of common software design patterns to CSS, like encapsulation, modularity, reducing the number of states etc.

Some people try to achieve the same predictability with inline styles, but you can get these benefits with normal classes. Just make sure to keep component styles isolated.

I have applied this pattern on a big project, when we build a social network for Redbull. It had a tremendous impact on the team’s ability to add and change code quickly without breaking existing features.

We created hundreds of components, but onboarding new developers took no time, unintended changes were basically non-existent, dead code was easy to track and we could move very fast.

Share on twitter