CSS Rot

After years of web development, I’ve found that one type of file goes bad much faster than others: CSS. Its unused files are ignored instead of removed. Its statements tend to be overridden instead of edited. And we pile on more classes instead of reusing old ones.

CSS is a wonderful language, but it’s dangerous. Let’s start with the idea that styles can’t be right or wrong. A computer doesn’t know if changing a z-index hides the modal on the sales page, or if applying overflow: hidden breaks the navbar in some way on iOS Safari. Other languages are more easily testable; it’s easy to ask a computer:

Tell me if a record is created with [these] properties when I run [this] function.

…versus:

Tell me if the UI on every related element is “still usable”, and “looks right”, when I change this box-sizing to border-box.

What’s more, CSS has some weird gotchas that can confuse new programmers when they start writing it:

<div class="example">
<p>Given this HTML...</p>
</div>
/* Text is blue. */
.example {
color: blue;
}

/* Rules defined later take precedence. Text is red. */
.example {
color: red;
}

/* IDs take precedence over classes. Text is white. */
#example {
color: white;
}

/* IDs take precedence, so text is STILL white. */
.example {
color: pink;
}

/* !important takes precedence over IDs and classes. Text is orange. */
.example {
color: orange !important;
}

/* Specific rules take precedence. Text is green. */
.example p {
color: green;
}

/* Specific rules take precedence. Text is STILL green. */
.example,
#example
{
color: yellow !important;
}

/* !important and specific overrides everything. Text is purple. */
.example p {
color: purple !important;
}

CSS is so powerful, open-ended, and globally namespaced, that it makes it easy to shoot yourself in the foot. Take specificity, for instance: one .example p selector may lead to an !important from another developer, which eventually creates the need for more and more nesting unless you want to create more classes for similar styles. CSS itself is fairly opaque: working with stylesheets means that you’re constantly opening your developer tools to find out which properties are actually being applied to an element. Vanilla CSS has no warnings, no errors, no console logs. It just sits there… silently.

A few years ago, I was refactoring the CSS for a web app where I found a 1.5k line CSS file with this beautiful snippet right in the middle:

.alert--error p {
color: white;
}

.alert--error p {
color: white !important;
}

.alert--error {
color: white !important;
}

/* AGGHHHHH */

.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }
.alert--error p { color: white !important; }

The fix for this type of problem is easy, at least in theory: make sure that your team understands the basics of the language. In practice, though, this is hard: once you’re working with a large enough team, someone is bound to use unnecessary !importants, too much nesting, or set styles via JavaScript. And once this starts, it spirals out of control, because stylesheet quality degrades exponentially. In other words, using one !important or inline style leads to needing to do it again, which continues until many of your classes are difficult to work with. Eventually, we start to add classes to our stylesheets instead of reuse them, and then we end up with an enormous stylesheet that causes higher bandwidth usage, degrades our developer happiness and productivity, and causes UI bugs. (Ever chase down a z-index issue?) This, dear reader, is one of the ways that CSS rots.

But this is only one issue you’ll encounter when working with CSS. Let’s talk about one of the others. Have you ever noticed that certain values keep repeating over and over again in your code? Some of these might be:

Repetition for single values can be eliminated with variables. These are now baked right into the vanilla language:

How to use CSS Variables

CSS now lets you add variables to :root, the HTML element. They need to begin with --, and you can reference them with var() like this:

:root {
--my-favorite-color: #ff0080;
}

.example {
color: var(--my-favorite-color);
}

The cool thing about CSS variables is that they cascade just like the rest of CSS. For instance, I maintain a light and dark theme for this site with virtually no effort by holding all of my colors in a variables.css file. I just add my dark colors to .dark:root and add a dark class to the html element when the user :

/* variables.css */

:root {
--background: #fff;
}

.dark:root {
--background: #000;
}

/* elsewhere, elements automatically respond to my theme */

.some-element {
background: var(--background);
}

Browser compatibility is good if you’re not supporting IE 11.

Eventually you’ll be able to reuse more than just variables in vanilla CSS, like mixins, although no browsers support it by default:

/* Define your mixin in :root */
:root {
--smallcaps: {
text-transform: uppercase;
font-size: 12px;
letter-spacing: 1px;
font-weight: bold;
}
}

/* Use @apply to apply your mixin */
.some-other-element {
@apply --smallcaps;
}

If you want more features, you can also compile your CSS, although you’ll need to watch out for files that become hard to reason about:

nav {
// ...

&.open {
// ...
}

ul {
// ...
}

li {
// ...

&:active {
// ...
}
}

svg {
// ...
}

a {
// ...

&:hover,
&:active
{
// ...

svg {
// ...
}
}
}
}

footer {
// ...

nav {
// ...

a {
// ...

&:hover {
// ...
}
}

ul {
// ...
}

li {
// ...
}
}
}

When we’re in the middle of a big project and we come across six different 200-line SCSS files shaped like this, the last thing we want to do is take the time to understand if we can use one of these classes. So we create new class names (or even new files altogether) instead of reusing the existing ones. And by creating new classes, we might feel the need to throw in a few !importants or do some further nesting to override the specificity on some existing elements. And our CSS continues to become harder to maintain.

Over time, we’ll also change the HTML. Some CSS classes will become stranded. Many older websites have thousands of declarations that aren’t being used, but we’re too scared to remove them because it might break some element we’d never even think about checking. Unlike most languages we’re used to, there’s no good way to test that editing a file, especially a deeply-nested one, didn’t break something somewhere else in the project. I’ve seen this a lot with SCSS. And sure, we can process files to remove unused CSS during a build, but it’ll still create mental overhead. I’d rather not have thousands of lines of unused code in my repo.

But let’s talk about one of the biggest issues with CSS as a language. We tend to think of our elements as pieces of a whole — as components. The navbar is separate from the main content, which is separate from the footer — but CSS targets the global scope. That means that if we have a layout like this:

<!-- Top navbar -->
<nav>
<ul>
<li><a href="/" class="active">Home</a></li>
<li><a href="/">About</a></li>
</ul>
</nav>

<!-- Navbar in footer -->
<footer>
<nav>
<ul>
<li><a href="/">Terms</a></li>
<li><a href="/">Privacy</a></li>
</ul>
</nav>
</footer>

…we can’t write CSS without adding some unique class names or deeply nesting our selectors.

a {
/* ... */
}

nav ul {
/* ... */
}

nav li {
/* ... */
}

nav a {
/* ... */
}

nav .active {
/* ... */
}

nav a:hover {
/* ... */
}

footer nav a {
/* ... */
}

footer nav .active {
/* ... */
}

We can add unique classes to style each of these elements, but each class will be in the global scope. In the past, we’ve fixed this by turning to modular CSS, like BEM, SMACSS, CSS Modules, or Styled Components.

Primer on CSS Modules

A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.

CSS Modules is a neat solution for creating modular CSS. In the following example, styles.foobar and .foobar would become something like .foobar_2x86z:

import React from 'react'
import styles from './Example.module.css'

const Example = () =>
<div className={styles.foobar}>
...
</div>

export default Example
.foobar {
color: red;
}

Even if there was another file with the foobar class elsewhere in the application, its styles would stay separate, because it would get its own transformed class, like .foobar_1i3lo.

This approach lets you create a small CSS file for each one of your UI components, keeping your styles separated and organized.

At this time, it seems like modular CSS is one of the two best approaches to styling large applications. When combined with CSS variables and componentization, it can handle mostly every use case you’ll need. But what about applications that don’t use a lot of componentization?

That brings us to what I think is the other best approach, an inversion of modular CSS. It is the use of utility classes that can be reused over and over again. I’ve never seen a well-crafted utility CSS class rot. For instance, if you wanted to add rounded corners to multiple elements:

.rounded {
border-radius: 8px;
}

Yeah, that’s the whole thing. And you never want to add more to it. Its benefits are because of its simplicity, not in spite of it. If you have a framework like this, you can piece together a UI by applying classes to HTML elements, and since it’s not nested, you don’t need to worry about overriding styles, using !important, or anything like that.

There are many utility frameworks. My favorite is Tailwind, which I’ve found to be stable, feature-rich, and well-thought-out for working on professional projects. Tailwind is also great for teams because of its docs: normal CSS classes aren’t easily searchable, but Tailwind’s docs make it easy to find the class you need.

If you’re planning on writing your own utility classes, my recommendation would be to prevent any one class from doing too much. For instance, a class like this isn’t a great idea:

.entry {
display: block;
padding: 1rem;
margin-bottom: 1rem;
}

Instead, split it into multiple classes like this:

.block {
display: block;
}

.margin-bottom {
margin-bottom: 1rem;
}

.padding {
padding: 1rem;
}

This might seem counterintuitive at first, but now you can use any combination of the three across your application, reducing the need to revisit your stylesheets.

The downside of using utility classes is that they make your HTML a bit uglier. For example, instead of:

<ul class="entries">
<li>
<a href="...">
Lorem ipsum
</a>
</li>
</ul>

…you’ll need to use something like:

<ul class="list-none">
<li>
<a href="..." class="block padded hover:bg-gray rounded">
Lorem ipsum
</a>
</li>
</ul>

I was against this approach at first, for several reasons:

However, a few days of real-world usage showed me that these weren’t the case. Semantic HTML alleviates my concerns with readability; I’d rather call something <header> than use div class="header". If anything, seeing header class="pb-4" takes me closer to the truth: that the header has padding on its bottom, so I don’t even need to check the CSS. And if I’m working in deeply nested divs, I can always use comments.

The learning curve to use utility class names took me about 15 minutes with Tailwind. Most class names are easy to remember; to set display: flex, you use class="flex". To set position: relative, you use class="relative". Commonly-used properties are shortened, like padding-bottom to pb. If you don’t know a property, you can easily search the docs to find it. Note: Choose a utility framework with good reference materials.

Finally, there’s a subtle distinction between utility classes and inline styles: utility classes are a single source of truth for any style, so you can edit and change them and your whole UI will respond appropriately. There’s no duplication here, no need to edit a value in multiple places.

A well-documented utility framework is a huge boon for a multi-developer project. Its benefits far outweigh the slight change to HTML. Good utility classes remove the need to maintain CSS and give you a single source of truth for every style declaration.

Designers should work with CSS, not against it

CSS rot can also come from something else. The quality of the designs you’re given correlates with the quality of the CSS you can produce. In other words, a good web designer understands the medium and creates designs that work good on the web.

Take one look at the Dribbble homepage, and you’ll see some designs that would work well, and others that would be a nightmare to maintain. A design might look beautiful, but it would take days and thousands of lines of CSS to implement it properly, to make it work on various screen sizes and devices, and so on. At some point, every developer will need to deal with a nightmare UI, so I want to give you some tips.

  1. If you’re handed a design that will hurt the maintainability of your CSS, bring it up with your designer. Go to them with a compromise. Even though engineers are usually quiet about stylesheet issues, CSS debt is real, and it’s nasty, and it creates a ton of work and UI bugs that everyone will have to deal with far into the future.

  2. If coding a design is unavoidable, use utility classes or modular CSS where possible. Comment any media queries or hardcoded values to explain what each rule does, and why it needs to do it. If you need to do this, understand that the file is basically radioactive and you need to be vigilant about reviewing it and making sure other developers follow suit.

What can you do today?

If you want to prevent CSS from rotting in your application, I have a few recommendations.

First, take a look at ways that you could adopt utility classes and/or modular CSS. Utility classes are global, and modular CSS is local, and they work quite well together. For instance, Tailwind and CSS Modules.

Second, look at condensing your current CSS. I did this at a previous company and decreased their CSS files by 80%, down to the core utility classes, and then we styled custom components with CSS Modules. (Refactoring CSS is a tedious process and works best during a redesign. If you’re interested, let me know and I can write a post about this.)

Once you start condensing and refactoring your CSS, I also have a few golden rules for working on team projects:

  1. !important is usually a sign that your CSS is rotting somewhere. You can use this to lead you to problem areas in your stylesheets, ripe for refactoring.
  2. Don’t use inline styles unless you have a good reason to.
  3. Try not to nest selectors more than once, or it will be a pain to update other styles. For instance, .foo .bar {} is okay, but .foo .bar .baz {} is not. If you can reject nesting altogether, that’s even better – but it’s probably unrealistic for most apps.
  4. Only style with classes. Don’t use element tags or IDs. IDs take precedence over classes, and element tags are easy to “accidentally style” in the future. Basically, if you don’t point a gun at your foot, you can’t shoot yourself in the foot.

In the end, CSS’s problems are those of discoverability. It’s hard to know if you already have a class that can do what you need, and if your edits broke some other part of the application. But we’ve started to move towards more maintainable practices: utility classes and modular CSS, which helps us build confidence in the interfaces we create.

I'm Mark Thomas Miller, an engineer, designer, and maker of things.