The Case for Functional CSS

I’ve been writing semantic CSS for most of my career. As an industry, we promoted it as best practice.

Slowly, skeptically, I’ve been converted to Tailwind. Somehow, it makes writing maintainable CSS simple, especially in large codebases. After some reflection, I believe it’s because Tailwind (and more broadly, functional CSS) is a good abstraction.

Pulling inline styles into a semantic class like .button--warn might feel like an abstraction, because it cleans up the HTML and allows reuse. But this “abstraction” is too vague. It doesn’t make the button’s style much easier to understand (e.g. how does it differ from .button--alert?). Upon reading the implementation, we could discover that the button is red, or blue, or a shade of purple that doesn’t exist in the design system.

The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.

– Dijkstra

Functional classes like .bg-red and .text-white clutter the HTML, so they don’t feel like an abstraction. But looking closer, we see that composing classes in this way leads to precise outcomes. It’s clear what <button class="bg-red text-white"> will look like.

More importantly, since these classes encapsulate design system primitives like color, whitespace, layout, etc., they are (in theory) all we need. They comprise a new semantic level for building designs. They are declarative. They don’t leak implementation details. They are a good abstraction.

Now, let’s see how these abstractions affect the codebase over time.

Semantic CSS creates complexity.

Functional CSS reduces complexity.

Proof in practice

When I re-enter an old codebase using semantic CSS, it takes me ages to relearn the API between HTML and CSS. I never feel completely safe refactoring or deleting code. And since the API is bespoke to the codebase, none of the knowledge transfers out.

When I re-enter an old codebase (or revive a feature branch, or review code) using Tailwind, I already know the API and comprehend the code with ease. Refactoring and deleting is free. The code behaves and will continue to behave precisely as expected.

It’s not perfect

Like any abstraction, functional CSS has trade-offs:

For me, the pros far outweigh the cons. I now consider semantic CSS harmful and avoid it when possible.