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. In my experience, this comes from a few things:

  1. CSS is hard to test.
  2. Not everyone knows how it works.
  3. Global namespacing and preprocessors are a double-edged sword.
  4. There is no logging or great way to debug styles.
  5. It’s easy to leave in unused CSS.
  6. Not all designers understand the grain of the web.

These are all difficult problems to solve. But recent trends have shown us how to make our stylesheets more resilient. Let’s go over each of these in detail, and follow up with some recommendations.

Testing

We’ll 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 easier to test; 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 affected element across my site is “still usable”, and “looks right”, when I change this box-sizing to border-box.

This means that when we change a style, we need to manually test it. This means visiting every instance of an element that was impacted by a style change — but this is a huge pain, so most developers decide to just create a new class and overwrite any existing properties. This means that it’s easier to duplicate styles than refactor them.

Continue this for a few years, and stylesheets become a mess of spaghetti code, bigger and buggier than they need to be.

Nuances

It doesn’t help that CSS has a lot of nuances that can be pretty confusing to new developers. For instance:

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

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

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

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

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

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

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

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

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

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

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

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

/* WHY WON'T THIS WORK */

.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 it just creates the need to keep repeating this behavior. In other words, stylesheet quality degrades exponentially.

Global Namespacing

When we’re developing a UI, we tend to think of each element as separate. The navbar is separate from the main content, which is separate from the footer – but CSS targets the global scope. That means 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 a:hover {}
footer nav .active {}
/* ... */

We can add unique classes to style each of these elements, but each class will be in the global scope, so we have to be careful with our naming conventions.

In the past, we’ve tried to solve these naming issues with methodologies like SMACSS. This was a great solution at the time, although it’s another thing to learn that doesn’t have immediate benefits, which makes it harder to sell to your team.

Because styles are global, one nested selector leads to another, which leads to an !important, which eventually creates the need for more nested selectors and more !importants, and the problem continues to grow.

Preprocessors

Preprocessors add features to CSS. They make it easier to lighten and darken colors and do math inside our stylesheets; they give us access to loops, and also higher-level features like mixins, which are helpful for grouped styles, like a certain font family/font size that are often used together, or horizontal menus that all use the same type of list styling.

But, they also make it very easy to bloat and overcomplicate your code. They often lead to unnecessary nesting and larger compiled file sizes:

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.

Vanilla CSS, though, has advanced over the years. One of the biggest improvements was the creation of variables:

How to use CSS Variables

You can use variables in vanilla CSS by defining them in :root. They need to begin with --, and you can reference them elsewhere with var():

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

.some-element {
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 storing my color palette for both themes in variables. I 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.

One feature that will hopefully come to vanilla CSS is mixins, which allow you to applies groups of styles with one assignment. The browser support is nonexistent for now, but this is an idea of how it could look:

/* 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;
}

Debugging

It doesn’t help that in its vanilla form, CSS is a black box. Some styles don’t work unless your element has a certain display value, or position, or so on. There are no warnings for this. If you define a rule that isn’t specific enough, nothing happens. After all, this is a feature! Some people solve this by rejecting cascading altogether with the argument that it never works to our advantage. And we catch ourselves using actual styles to debug our other styles:

.example {
border: 2px solid red !important;
}

Combine this with hundreds of properties to customize, and you wind up with a language that’s hard to debug. Try solving a z-index or overflow issue in a complex app, and you’ll spend most of your time in your developer tools trying to find out why properties aren’t being applied to an element – or why they are, but nothing is happening.

JavaScript gives you a debugger or console logs to trace issues, but CSS is completely silent.

Stranded

As pages change over time, some CSS rules become stranded, meaning they’re no longer connected to any element. However, there are no logs to tell us, “hey, you’re not using that rule anywhere, you can remove it”.

Many older websites have thousands of declarations that aren’t being used, but developers are too scared to remove them because it might break some element they’d never even think about checking. I’ve especially seen this with deeply-nested SCSS — it makes sense when you’re writing it, but once you lose context, it’s hard to get it back.

And sure, there are tools to remove unused CSS during builds, but it’s still another thing to set up (so many people don’t). And the styles are still there when you’re working in the repo.

Designers

A good web designer understands the medium they’re working with and creates designs that work with the web’s grain. In other words, the quality of the designs you’re given correlates with the quality of the CSS you can produce.

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 different screen sizes, and so on. Because stylesheet quality degrades exponentially, you’ll need to be cautious when developing this kind of design. (It’d be like building a house from a blueprint that looked cool, but wasn’t structurally sound.) At some point, every developer will need to deal with a nightmare UI.

The best approach is to go to your designer with a compromise. But if not, you’ll want to follow some of the advice in the next section.

How CSS can be fixed

Today, I believe that we can solve the problems of CSS with two things: utility classes and modular styles.

Utility classes are minimalistic CSS classes that are created for the purpose of adding one type of style to an element. For instance, if your UI has rounded borders on some of its elements, you could create a utility class:

.rounded {
border-radius: 8px;
}

I was against utility classes for a long time until I actually used them in a project. Here are some of my old arguments against them, with my new thoughts:

A good utility framework is a huge boon for a multi-developer project. Its benefits far outweigh the slight change to HTML and the short learning curve, and they make your stylesheet size grow at a much slower rate. In addition, I’ve never seen a well-crafted utility class rot.

More thoughts on utility classes

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. It’s 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.

Creating your own utility classes

If you don’t want to use a pre-built utility framework, 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. This means you won’t have to worry about styles overwriting each other or use !important.

Modular styles work in the opposite direction of utility classes: instead of assigning global, generic styles, it assigns localized, highly specific styles to each component. There are many packages for this, two of my favorites being Styled Components and CSS Modules. Let’s take a look at one of them:

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 lets you create a small CSS file for each one of your components, keeping your styles separated and organized. Take this example:

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

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

export default Example
.foobar {
width: 10px;
height: 10px;
background: red;
}

className={styles.foobar} will become something like .foobar_2x86z in the browser. 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.

When combined with CSS variables and componentization, this can handle mostly every use case you’ll need. It allows you to use simple naming conventions, enforces organization, and prevents every issue you used to have with global namespacing.

Next Steps

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, combine Tailwind and CSS Modules. Use Tailwind for everything you can, and use CSS Modules when you need ultra-specific behavior for a certain use case.

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 can usually be followed to find CSS rotting.
  2. Don’t use inline styles unless you have a good reason to.
  3. Try to cut back on how often, and how much, you use nesting. If you can reject it altogether, that’s even better – but it’s probably unrealistic for most apps.
  4. Only style with classes. IDs take precedence over classes, and 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.

Put together, these ideas will help you navigate the open-endedness of CSS, and to write resilient code that stands the test of time.