Tom Genoni / Design Technologist
Photo of a mosaic
March 8, 2020

Functional CSS in Practice

Introducing a controversial tool to a tough crowd.

Establishing a new technology in an engineering team is not easy. Frankly, it’s a bit scary. Even if you manage to convince the skeptics of its value there’s the looming risk that some fatal flaw will rear its head late in process, leaving you embarrassed and your team cleaning up the mess. Was it worth trying to fundamentally change the way we wrote CSS at Thumbtack? What could possibli possibly go wrong?

“Worst web dev idea, or just a really bad one?”

Though the concept of functional CSS, also known as utility-first or atomic CSS, has been around for years it gained significant traction with the introduction of Tachyons and subsequent projects like Tailwind. The idea is that developers favor terse, predefined classes over inventing new classnames and writing declarations manually.

Like many who spent years writing semantic CSS, I had an allergic reaction when I first encountered its haphazard groupings of cryptic classnames. These were not the best practices we’d been told to follow. It seemed like it would only worsen the problem of managing CSS at scale. And much of the internet agreed.

Not encouraging.

Prework

The only way I was going to navigate these headwinds was to try it myself. And as I continued to consider the problems functional CSS was trying to solve I realized a few things that helped me unpack the criticism.

Framework or library

I hadn’t previously given much thought to how technologies get categorized. But the distinctions can be important when evaluating how significantly it will affect a technology stack.

Generally speaking, frameworks are only effective when they control the code. Angular, for example, expects to be implemented in a specific way and complains when it’s not. Libraries are optional and make no programmatic demands on where and when they’re used. Tailwind describes itself as a framework, Tachyons as a library—which is it? I certainly didn't want our team locked into the “straitjacket” Jay Firestone warned about.

It became clear that functional CSS implementations were clearly libraries, tools that could be employed as the developer saw fit with manual CSS always available as a fallback. Further, Tachyons was just one expression of this concept; we could easily build one to suit our needs.

What is “meaningful”?

A cornerstone of coding best practices has been the encouraged use of classnames derived from content. It’s even part of the HTML5 spec. Among the benefits, semantic names like card and avatar allow one developer to quickly orient another by communicating through self-documenting code. As an early fan of Jeffery Zeldman I’ve long advocated and practiced this approach.

The basic steps are:

  1. Review the designs to determine the content types and structure.
  2. Invent classnames that are specific to the content yet general enough to be reused later.
  3. Add that class to a CSS file and write your CSS to set its properties.

If there was ever the antithesis of this workflow, classname soup like pa3 ba b-gray-300 bw-2 was it. But the concept of “semantics” deals with whether something conveys any meaning, not how it’s derived. In his 2012 post About HTML semantics and front-end architecture Nicolas Gallagher contends that content-driven classnames are unnecessary and, in fact, “often a hindrance.”

We shouldn’t be afraid of making the connections between layers clear and explicit …. Doing this doesn’t make classes ‘unsemantic’, it just means that their semantics are not derived from the content.

Nicolas Gallagher

The class pa3 is a readily understandable string to the audience that needs to understand it. While it does not contain information about the content it’s acting on, perhaps we can derive other benefits in consistency, reliability, and ease of maintenance. It's a tradeoff.

If by now you’re thinking functional CSS has thoroughly soiled the flag of semantic best practices, CSS-in-JS has arrived to light it on fire. In these implementations, popular in React (Gallagher is now a React Core team member at Facebook), classname generation is handled programmatically and the resulting client-side code is effectively meaningless to humans. Criticism of Twitter’s rollout of it ranged from the philosophical to the practical to the blunt. But it’s undeniable that many companies, including Thumbtack, have benefitted from this novel manipulation of CSS. Zeldman’s claim that “nonsemantic classnames … will always be a bad idea” was looking increasingly difficult to square.

To each their own

Within companies you’ll find wide variation in the size of the design and engineering teams, how they’re structured and the technologies they can use, the number and kinds of products they support, and the types and scale of their customers. What works for medium-sized startups and large established organizations may not in agencies and single-person shops. Obvious points, yet much of the horrors forecasted by functional CSS critics seemed based on small-scale testing and dogmatism rather than real-world failures in diverse conditions.

At the time I was evaluating functional CSS, Thumbtack was well into our move from PHP and Angular to React. This meant styling and interactivity could be tightly controlled and contained in components, reducing the need for BEM class naming strategies. Plus, our engineering team had a number of full-stack developers contributing to the codebase who, not well-versed in CSS, were producing code of varying quality. Finding ways to write less of it was appealing.

Let’s try it

After rebuilding a few pages on thumbtack.com using Tachyons and experiencing its benefits firsthand I was satisfied it was worth pursuing.

Selling it

I first met with engineers who had previously expressed concerns. This unearthed issues I either hadn’t considered or were particular to our technology stack. I also interviewed a number of external developers at companies using existing libraries or who had rolled their own. It turned out that functional CSS was not a fringe idea relegated to small websites. In one form or another it was already being used at Buzzfeed, TED, StackOverflow, Segment, Algolia, Kickstarter, and GitHub.

The end result of this exploratory process was a lengthy RFC that detailed the problem, the solution, and the pros and cons. Over the course of a couple weeks it allowed a broader engineering audience to weigh in and for me to tie up any loose ends.

Building it

Finding no significant objections I began building a custom library based on Tachyons. Responsive characters were moved to the front (m_mt4 instead of mt4-m), a few categories like “floats” were deleted, and after some debate grid classes were added. (The notion of a grid with its parent-child dependency doesn't really fit in with the “one class, one property” spirit of functional CSS, but since it would be used it so frequently I felt we could make an exception.) Because we had a mature collection of design tokens we were able to ensure any future changes to color or spacing units would automatically propagate to our functional classes.

Documentation and education were next. To be proficient with functional CSS it’s critical to quickly find the classes you need. For my beta testers I generated a reference with all the classnames, the output of those classes, and usage examples. To answer any lingering questions I also held demonstrations as part of the rollout.

As a final trial, three engineers worked with the library for two weeks in their thumbtack.com tasks. I checked in frequently to collect feedback and addressed a handful of minor issues. In late August of 2018 we officially released Thumbprint Atomic for use by all developers at Thumbtack.

Results

Twice a year the Design System team sends out a “Building UIs at Thumbtack” survey to our front-end and native engineers to help us gauge the effectiveness of the tools we provide. In early Q1 of 2019, roughly four months after Thumbprint Atomic was introduced, the survey results included 14 comments referencing it.

We were pleased to see 12 were positive. Atomic was characterized as “a good move” and “immensely helpful.” A few noted the “ease of using atomic,” that it “sped up” their workflow, had “made building interfaces a lot easier,” and at least three declared Atomic was “great.” The remaining two comments cited the learning curve and the need to frequently reference the documentation, concerns we anticipated.

Atomic is probably the biggest win in terms of dev speed. It takes a little while to gain fluency, but once you can recall class names by memory, building UIs speeds up dramatically.

Thumbtack frontend engineer

The good

At Thumbtack, frontend developers use a hierarchy of technologies. They first check to see if a React component exists, either in Thumbprint or their codebase, that meets their needs. Where previously their second option was to use JavaScript or Sass tokens, often resulting in more CSS, they could now use Atomic classes.

After a year and half of exercising that option none of the scary outcomes predicted by the critics came to fruition. And my own experience of using functional CSS, both with the frontend engineering team and in personal projects, has been overwhelmingly positive. It’s best considered an additional tool, useful when the traditional semantic workflow is more a burden than beneficial. (As much as I once enjoyed “crafting” CSS I’m perfectly happy never writing display: flex; again.)

Thumbprint Atomic has also been very stable. Our team anticipated adding and removing classes more frequently as our usage evolved. But with the exception of a few changes it continues to cover the majority of the repetitive CSS we would otherwise be writing.

(The following list was inspired by a comment I left in the Tachyons GitHub issue “How is tachyons different from inline styles?”)

Speed. As noted above, once you’ve learned the classname syntax, building up UIs is very quick. Prototypes and production code are sometimes one and the same.

Easy responsiveness. To increase padding with media queries using our spacing scale, the Sass code would look something like this.

.card {
    padding-left: $space_2;
    padding-right: $space_2;

    @include respond-to($break-medium) {
        padding-left: $space_3;
        padding-right: $space_3;
    }

    @include respond-to($break-large) {
        padding-left: $space_4;
        padding-right: $space_4;
    }
}

With Atomic I simply add ph2 m_ph3 l_ph4 and move on.

Less CSS bloat. How often is old CSS deleted? In my experience, infrequently. It’s too scary. Unless carefully managed, CSS balloons and rarely gets the attention it needs. Too few developers, myself included, ask themselves “How easy will this code be to delete?”

Ownership less important. Yes, communication is critical but as employees come and go you don‘t always have that luxury. Trying to parse the personal coding decisions—classnames, layout approaches, complex selectors—of a developer who’s moved on can be confusing. It’s far less problematic when looking at classnames of consistent meaning and limited scope.

Refactoring is easier. As time passes it’s often difficult to identify all the places that HTML relies on some shared CSS. Removing it is now risky. By using functional CSS the effect of adding or removing a class in an HTML template is unambiguous.

UI consistency. Developers are limited to the playground of classes that are mirrored in the tools designers use. Unlike inline styles or external CSS which can include any values, styling is predictable and tied to your design system.

Code consistency. There are probably seven ways to center an element. Which one should I use and does it matter that the developer across from me used a different but equally valid approach? Not every layout can be distilled into a One True Way but the functional CSS options are limited and clear.

Cross-team consistency. If you have multiple teams with output that commingles you can limit style drift. At one point Yahoo was using this approach to align their various independent verticals (Finance, Sports, etc.) that produced content for Yahoo’s homepage.

Easier to contribute. CSS is deceptively complicated. The learning curve for writing and maintenance gets steep once the layouts and site complexity become non-trivial. By not requiring all CSS be written manually we open the door to more contributors and limit the damage a mistake can cause.

Naming is hard. As discussed above, to add CSS the traditional way you have to invent meaningful hooks for virtually everything. Functional CSS can avoid that entirely, and you can always add content-specific semantic identifiers for code clarity if needed.

Fewer specificity problems. How deep should my selectors go in my CSS? Can code later in my CSS easily override earlier code if needed? Will I find I've unintentionally painted myself into a corner with the code decisions I made two months ago? These concerns are mostly side-stepped.

Avoids global scope. By using existing class names we avoid introducing new unscoped CSS that could collide unexpectedly with existing or future code.

HTML not “drastically larger”. Despite Adam Silver’s warning, adding functional classnames to your HTML does not significantly increase HTML size. On one complicated page the document size went from 48k to 49k after Atomic conversion. By replacing 90% of custom styling with functional classes, the CSS went from 48kb to 13kb. Since the 9k Atomic library is cached for users on their first visit, the result was actually a faster loading page.

There is an escape hatch. Developers want to write code that can be reasoned about. There will be layouts and interactivity that are simply too complex to be accomplished through functional classes alone. And there will be design experiments that extend beyond the design system. In these cases you can always revert to traditional methods. Functional CSS is not an all or nothing technique.

The not as good

Despite all the benefits functional CSS it’s not a silver bullet that’ll fix all your style-sheet woes. Consider it a Swiss Army knife, handy in a lot of situations but not the only tool you'll need. Here are a few areas where it can be problematic.

The syntax. There’s no getting around it: it looks ugly. Worse, some classes use a literal value (bw-1 is border-width: 1px) and others a scale (mb1 is margin-bottom: 4px). Though these variations eventually make sense the rationale is not initially obvious. I found it took a few days of somewhat uncomfortable use to commit the most used classnames to memory.

Duplicated strings. Functional CSS is best used with a component and templating system in place to provide reusable code. If your project consists of HTML blocks repeated throughout your project this approach will get messy. You probably don't want f6 link dim ph3 pv2 mb2 dib white bg-blue duplicated in hundreds of files, requiring error-prone find-and-replace exercises if you need to make changes.

Complex layouts. The parent wrapper of a component is often where you'll have the most styling activity. If you need to responsively change an element’s position, padding, borders, widths, and background colors it may be better to write out the CSS, even if you could accomplish it with functional classes. Balancing development speed with code clarity will require judgment calls.

Complex JS interactivity. For simple interactions functional CSS classes work just fine: show this element, make this text red, and so on. But similar to the problem with complex layouts, if you're adding and removing a long list of strings you‘ll likely find it better to capture those styles in a class.

Takeaways

The process of researching, building, testing, and releasing Thumbprint Atomic was not straightforward. It took months. It was sometimes stressful. I wasn’t sure it would work. But it also produced two valuable lessons.

The first is that patience and diplomacy work wonders. People are understandably protective of the way they work and taking risks on new technologies can be disruptive. Despite my eventual confidence that functional CSS would be useful at Thumbtack, and my desire to move quickly, I realized it was important to make room for others to arrive at that same conclusion. Building the case slowly and as thoroughly as I could—especially given all the critics aligned against it—helped convince the team to move forward.

The second is that best practices can blind you to new ideas, or ways of using old ones. Ordinarily these standards reflect the collected wisdom of many users over many years, helping to optimize workflows and avoid costly mistakes. But conditions shift and notions that were once thought unworkable, or rules considered sacrosanct, merit another look.

In Blame the implementation, not the technique author and web performance consultant Tim Kadlec argues it’s easy to get sidelined by accepting conventional wisdom. “[Technologies are] only as good as the person using them. If you get to know them, identify their strengths and weaknesses and use them when appropriate, they can be really powerful and helpful additions to your toolbox.”