Redesigning Our Docs – Part 4 – Building a Scalable CSS Architecture

This is the fourth article in a seven-part series of blogs that describe our most recent changes to the architecture and content of our documentation. We focus here on CSS architecture and improving the way we deal with assets.

When working on a documentation website, it’s easy to focus solely on content and to put the rest on the back burner. This is especially true for the Algolia docs: over the years, it has undergone some dramatic changes. With every new feature, API client, and product, the website has had to quickly expand and transform to cater to new needs.

Keeping up with the growth of Algolia has also meant going through many redesigns, often by many different people. It contributed to a front-end codebase that became increasingly hard to maintain. Therefore, when we decided to redesign the website at the end of 2018, we deemed this was a great time to take a hard look at the state of our CSS. It had become increasingly difficult to change something without breaking something else. A simple change could turn into an hour of keeping duplicates up to date and battling media queries. We were no longer in control, and every edit felt like adding a band-aid on top of a bunch of others.

We therefore decided that our docs redesign was an excellent opportunity for us to wipe the slate clean and start over. After all, we were getting a brand new design, so it made sense to rewrite our entire CSS codebase and define an unambiguous and robust methodology that could keep up with our growth for the next few years.

Pinpointing pain points

CSS is notoriously hard to scale. It offers powerful tools, but making mistakes doesn’t forgive easily. Global scope and specificity are good examples: when mastered and used purposefully, it can do wonders. When used reactively, problems start piling up, and it becomes tough to opt out of them.

In our CSS, we used to have much reactive specificity. Instead of using it with control, we used it to fix issues.

main.content-container p:not(.report-link):not(.edit-link):not(.copy-link) {
  margin-top: 17px;
}

main.content-container td p:not(.report-link):not(.edit-link):not(.copy-link):first-child {
  margin-top: 0;
}

The above snippet is a typical example of a specificity battle stemming from a wish to apply rules globally, then finding exceptions. The problem with these kinds of overly-qualified rules with many descendants is that not only is it fragile and hard to maintain by hand, but it can also become costlier in terms of performance, as it takes longer for the browser to match.

Another underlying issue which comes with writing reactive CSS is that your codebase keeps on growing. As you add new components that look slightly different from existing ones, or keep patching up issues to fix earlier misguided decisions, assets become increasingly heavier.

When we audited our CSS, the conclusion was beyond dispute; the cause of our issues was that we had no visible CSS architecture. To be exact, we used a mix of several CSS methodologies. Over the years, successive contributors had sprinkled some globally applied styles, some component-oriented CSS (OOCSS), some utility-first CSS, and plenty of hacks and overrides. While all contributors had the best of intentions, unfortunately, without clear guidelines and no assigned front-end owner, their contributions quickly resulted in tangled CSS with repetitive and unused classes, side effects that were hard to track, and a codebase that grew every time we added something new.

It was time for us to set up a proper architecture, enforce conventions, and to start treating CSS as a first-class citizen.

A composable approach

After assessing several popular methodologies (including pure, Bootstrap-like OOCSS, BEM, etc.), we decided to go with utility-first CSS, a methodology which encourages composition through the use of atomic classes, and lets you abstract into components when necessary. We integrated it in a loose version of the ITCSS architecture.

Among the things that seduced about utility-first, we enjoyed that it pairs well with branding guidelines. Utility-first is designed to directly map onto stylistic rules instead of letting you write rulesets freehand, without any framework.

A strong foundation

Instead of manually maintaining a raw stylesheet of atomic styles, we decided to go with Tailwind CSS, a utility-first framework that generates CSS classes from a JavaScript manifest file. Tailwind has great documentation, an active community, and is compression-friendly (more on this later).

One of the advantages of computing CSS instead of maintaining it by hand is that it works wonders with design systems. We get to use the full power of JavaScript to generate styles based logically on predetermined rules. This method is much more manageable and time-effective than maintaining rulesets by hand. Need more spacing utilities? No need to manually write them one by one. Simply increase the counter.

const spacingUnit = 8
const variations = 120
const generatedSizes = {}

for (let i = 1; i <= variations; i++) {
  const size = i * spacingUnit
  generatedSizes[size] = `${size}px`
}

We also get a configuration file that we can share across projects. This is extremely useful, as the documentation scope includes several separate projects (like the interactive tutorial or the widget showcase). Being able to reuse rules and cherry pick what we need is far easier to do with a JavaScript configuration file than with a bunch of scattered CSS files.

Controlled file size

Online documentation should load quickly. When building something, developers often refer back and forth to docs for small bits of information. What was the name of that parameter again? And the return type of this method? These are questions for which they need fast answers, without ever going out of their zone.

Browsing online documentation should be a seamless experience. At Algolia, we’ve built our documentation for speed (statically-generated pages, Algolia-based search) but there was one area that we had neglected so far: assets size.

Steady size

Utility-first is a great way to keep your CSS steadily small. Because we’re only reusing atomic classes instead of creating new rules, we don’t introduce new CSS code that may grow the assets. Even when we introduce pages with a different layout and new components, as long as it follows our existing UI system, the CSS doesn’t grow.

Smaller browser memory footprint

Another perk of going utility-first is that the browser doesn’t have to work as much to resolve styles. Utility-first CSS enforces using classes only (no matching on tag names) and keeps specificity low (most of the time, utility-classes are one level deep). It reduces (if not eliminates) style overrides, which reduce the overhead of determining the final styles for a given DOM element.

Automated purge

In our case, we’re using TailwindCSS to generate our atomic classes. These are based on our brand colors, the grid size we’ve selected, the number of variations we need, etc. As you can guess, this can generate many classes, most of which we don’t need.

For that reason, we’ve set up Purgecss to automatically strip out any class we don’t need in production. This tool allows us to have access to the full catalog of available classes in development mode but filters out all unwanted bits from the production build.

What about HTML bloat?

Keeping CSS small is great, but what about HTML? Aren’t we just moving the bloat from CSS to HTML files? This is the primary concern I usually hear when advocating for utility-first CSS.

It’s important to keep in mind that CSS and HTML are different. Compression algorithms like Gzip and Brotli beautifully handle repeated class names, as they’re themselves based on algorithms that are specifically designed to compress duplicate strings. The resulting file size for HTML documents makes little or no difference whether you use a few or many classes.

Content-friendly CSS

Atomic classes are great, but what about user-generated content? We can’t ask our technical writers, nor our contributors from other squads, to add utility classes to the content they write. It is neither user-friendly nor maintainable, and it would also significantly hurt content readability during code reviews.

For this specific use case, we needed to step away from utility-first without creating new problems, nor allow breaking out from the systems we had put in place.

Preventing style leaks

Technical writers and content contributors write in Markdown, which then compiles to plain HTML. Therefore, we needed to have styles that apply directly to HTML tags, without them leaking on other areas of the website.

Since CSS applies globally, the only way for us to scope rules was to use specificity as a way to namespace all CSS rules for content. In our Haml templates, we added a specific CSS class on the containers that surround outputs from Markdown file and used this class in our CSS to contain rulesets that would otherwise apply much widely.

Here’s what a Markdown content file looks like:

## Some title

Some content

And how we render it, in a Haml layout:

.content
  = yield

These compile down to the following HTML code:

<div class="content">
  <h2>Some title</h2>
  <p>Some content</p>
</div>

Then, we can style the content safely (here in Sass):

.content {
  h2, h3, h4, h5, h6 {
    // ...
  }
}

This technique works well, but has one downside. Since it relies on specificity, each ruleset scores higher than any one-level deep utility class. In the above example, a generated ruleset .content h2 has a specificity score of 0,0,1,1 while a utility class .margin-0 has a score of 0,0,1,0. If we wanted to override the margin of a specific h2 element with a utility class, specificity would get in our way.

Tailwind lets you circumvent this issue by allowing you to make all utility classes !important. However, keep in mind that this is an aggressive option that you need to manipulate with the uppermost caution.

In the future, we may look into CSS Modules for this part, so we can namespace class names directly without relying on specificity. Going this route would involve making it work with Markdown in a Haml/ERB context, as well as making them play nice with TailwindCSS.

Single source of truth

When writing scoped CSS rules by hand, it becomes easy to derail from the framework that utility classes impose, and to duplicate code. Fortunately, this is where one of the best Tailwind features comes in handy.

Tailwind provides an @apply directive, which, contrary to @extend, mixins, or placeholders in Sass, copies the CSS declarations from the reference class into a new ruleset. If you’re familiar with Less, @apply works like Less mixins: it uses actual classes that you can apply directly, and copies all their declarations into a destination ruleset.

Take the following class:

.text-nebula-blue {
  color: #5468ff;
}

Instead of copying its content into new rulesets, we use @apply to reuse it:

.content {
  a {
    @apply text-nebula-blue;
  }
}

This compiles down to:

.content a {
  color: #5468ff;
}

Whenever we need to write custom CSS, we reuse utility classes via @apply instead of writing new rules. It forces us not to derail from our design system and end up with inconsistent and exotic styling.

Utility-first, second to none

We’ve had great success with going utility-first in the Algolia documentation, reducing our CSS from over 125 KB (and continuously growing) to less than 50 KB (9 KB GZipped, 7 KB with Brotli compression), which represents a 60% size decrease! From a user perspective, this is a guarantee of lightning-fast loading styles with extremely low overhead.

From a project perspective, our new CSS methodology has allowed us to achieve more consistency in the way we style things. It also lets us keep our styles aligned with the Algolia branding guidelines and the UI rules that our product designer has set out for us.

At Algolia, we consider documentation to be a fully-fledged product in and of itself, not only a byproduct of our APIs, clients, and libraries. For this reason, we strive to ensure that we don’t neglect any aspect of the website. This new CSS architecture is in line with our goals: continually improve developer experience (DX) and build scalable, sustainable systems.

A new CSS architecture isn’t the only thing that happened on the front end of our docs. Our next article deep dives into the making of the new interactive showcase we built to document InstantSearch.

  • Nuno Maduro

    Great article. Thank you for this.