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.
Functional classes like
.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:
Ideally, a functional CSS lib should contain the minimal set of design primitives that can be composed to express any interface. This is difficult to achieve and maintain over time, and requires disciplined API design.
Evolving a design system is a large-scale task that may involve editing all call-sites of a class; e.g. all instances of
blue-500must now be
green-500. (Note: although painful, compare this to performing the same task in a semantic CSS codebase)
The API has a high-learning curve and makes the HTML verbose.
Escape hatches to raw CSS are inevitable.
It might be too much machinery for simple websites. It might be too restrictive for creative websites/designs that don’t map to a design system.
For me, the pros far outweigh the cons. I now consider semantic CSS harmful and avoid it when possible.