What is Utility-First CSS?

You can’t really appreciate utility-first CSS until you have a decent understanding of CSS itself, so this article will principally be about that. However, paradoxically, the more you learn about CSS, the less you may appreciate utility-first CSS. You might begin to question why it should exist at all. It’s possible you’ll even start to question why you exist.

I’ll try my best to explain.

CSS is responsible for a great many things when designing for the web, including typography, layout, and animation. But it’s not just what it’s capable of that’s important; it’s also how.

Before CSS, we had to style HTML elements on a case-by-case basis using now long obsolete presentational elements like <center> and <font> and presentational attributes like bgcolor and face. This approach to styling was a massive arse-ache and everyone hated it, but they didn’t have a choice.

Then CSS introduced the selector: a pattern matching mechanism for styling multiple elements with one set of rules. It’s hard to overstate the utility of selectors, but that’s not what the term utility-first CSS is referring to, unfortunately.

Instead, in the contemporary CSS vernacular, a “utility class” is specifically an HTML class, corresponding to a single CSS declaration, attached to a class selector. This is not quite an inline style like bgcolor="black" or style="background-color: black" but it is a piecemeal style, applying just one specific value to one property—and it does necessitate adorning the HTML with the class.

.margin-inline-start-20 {
  margin-inline-start: 20px;
}

Typically, the majority of your CSS would be applied using far reaching selectors (:root, body, p, figure etc) and inheritance. In fact, I believe there’s a kind of CSS 80/20 rule wherein about 80% of your styling should be done with just 20% (or less!) of your CSS.

Where utility classes come in is they allow you to make occasional exceptions to these generalized styling rules. That’s their utility, and their only utility. They do not embody utility per se and they are not the most useful thing in CSS.

Utility-first CSS is more radical. Utility-first CSS is exception-first CSS. And that’s not how exceptions work, in CSS or in general.

Take paragraphs. Your paragraphs will almost invariably share the same font-family, font-size, line-height, and margin. Exceptions will be few. So how would you approach styling your paragraph elements? Well, the font-size and line-height are typically taken care of already, since <p> elements would inherit these values from an ancestor element, such as the :root. So your best course of action here is, I don’t know, make a cup of tea, go for a walk, something like that, just don’t write any code. As for the margin, you can style 100s, 1000s, even millions of paragraphs at once, in one place, with a single, simple declaration:

p {
  margin-block: 1rem; 
}

In fact, placing a margin directly on any element or category of elements is somewhat a bad pattern anyway. What you should probably be employing is contextual margin via a Stack component using a sibling selector (do not talk to class-centric utility-first adherents about sibling selectors).

In any case, what utility-first CSS does is look you in the eye, wink, and say this approach to styling is better:

<p class="font-sans text-base leading-6 my-16">And</p>
<p class="font-sans text-base leading-6 my-16">on</p>
<p class="font-sans text-base leading-6 my-16">and</p>
<p class="font-sans text-base leading-6 my-16">on</p>
<p class="font-sans text-base leading-6 my-16">and</p>
<p class="font-sans text-base leading-6 my-16">on</p>
<p class="font-sans text-base leading-6 my-16">and</p>
<p class="font-sans text-base leading-6 my-16">on</p>
<p class="font-sans text-base leading-6 my-16">etc</p>
<p class="font-sans text-base leading-6 my-16">etc</p>
<p class="font-sans text-base leading-6 my-16">etc</p>
<p class="font-sans text-base leading-6 my-16">etc</p>
<p class="font-sans text-base leading-6 my-16">etc</p>

Utility-first detractors complain a lot about how verbose this is and, consequently, how ugly. And it is indeed. But you’d forgive it that if it actually solved a problem, which it doesn’t. It is unequivocally an inferior way of making things which are alike look alike, as you should. It is and can only be useful for reproducing inconsistent design, wherein all those repeated values would instead differ.

To put it another way, show me a design for an interface that benefits from being coded using utility-first CSS and I will show you an interface that is fundamentally f**ked—and long before it falls in the lap of some hapless front-end developer.

What if your organization or client generates content using a syntax like markdown or a WYSIWYG text editor, that translates the input into arbitrary, unadorned HTML—which will be in most cases? Well, you’re kind of stuffed, aren't you? That is, unless you create, say, a specialized markdown plugin that combines utility styles and applies them using, say, Tailwind’s @apply directive, which... is just writing CSS but in an absurdly obfuscated way.

const plugin = require('tailwindcss/plugin')

module.exports = {
  plugins: [
    plugin(function({ addBase, theme }) {
      addBase({
        'p': { fontSize: theme('fontSize.base'), marginBlock: '1rem', lineHeight: '1.5' },
      })
    })
  ]
}

Tailwind is a particularly slippery customer because it’s a purportedly utility-first tool but with multiple escape hatches, like @apply, for writing CSS that combines utility classes into the kind of semantic declaration blocks the framework initially wants you to think it eschews.

.select2-dropdown {
  @apply rounded-b-lg shadow-md;
}
.select2-search {
  @apply border border-gray-300 rounded;
}
.select2-results__group {
  @apply text-lg font-bold text-gray-900;
}

Tailwind even uses my 10-year-old owl selector concept for applying space between child elements. Which means using a dreaded child selector!

So what is Tailwind really? It’s just CSS with extra steps and a brand name. Then again, you can say that about most any CSS framework.


Why is this utility-first approach so popular at the moment? Partly because the designs we’re charged with coding often are f**ked and we need equally f**ked tools to wrangle them. Partly it’s because the f**ked tools we’ve adopted to write f**ked JavaScript don’t play so well with CSS or, for that matter, HTML. Mostly, it’s because developer insecurity and neophilia are easily exploited: “Have you ever written CSS you weren’t quite happy with? Well here’s a radical, paradigm-shifting, quasi-proprietary solution! You’ll never embarrass yourself again!”

It turns out, people in tech are particularly bad at distinguishing between paradigm shifts and paradigm sharts. That’s why we have nose-diving cryptocurrencies, dust-collecting monkey JPEG portfolios, and AI-generated children’s books teaching kids about pink, two-headed dinosaurs that never existed.

It’s not that utility-first CSS can’t be used to style things. It technically can. If you don’t understand CSS, it might be the best way for you to do so. At least if you work exclusively in JSX. And are ensconced in several months and gigabytes of tooling. If you do appreciate CSS, frameworks like Tailwind include features and tools that help you move away from the piecemeal, utility-first approach they encourage you to use from the outset.

But, just as a thought experiment, imagine if CSS didn’t exist, today, in any form (including as an escape hatch inside a utility-class based framework) and your only two choices for styling were:

<button class="
  [&amp;>[data-slot=icon]]:-mx-0.5
  [&amp;>[data-slot=icon]]:my-0.5
  [&amp;>[data-slot=icon]]:shrink-0
  [&amp;>[data-slot=icon]]:size-5
  [&amp;>[data-slot=icon]]:sm:my-1
  [&amp;>[data-slot=icon]]:sm:size-4
  [&amp;>[data-slot=icon]]:text-[--btn-icon]
  [--btn-bg:theme(colors.zinc.900)]
  [--btn-border:theme(colors.zinc.950/90%)]
  [--btn-hover-overlay:theme(colors.white/10%)]
  [--btn-icon:theme(colors.zinc.400)]
  after:-z-10
  after:absolute
  after:data-[active]:bg-[--btn-hover-overlay]
  after:data-[disabled]:shadow-none
  after:data-[hover]:bg-[--btn-hover-overlay]
  after:inset-0
  after:rounded-[calc(theme(borderRadius.lg)-1px)]
  after:shadow-[shadow:inset_0_1px_theme(colors.white/15%)]
  before:-z-10
  before:absolute
  before:bg-[--btn-bg]
  before:data-[disabled]:shadow-none
  before:inset-0
  before:rounded-[calc(theme(borderRadius.lg)-1px)]
  before:shadow
  bg-[--btn-border]
  border
  border-transparent
  dark:[--btn-bg:theme(colors.zinc.600)]
  dark:[--btn-hover-overlay:theme(colors.white/5%)]
  dark:after:-inset-px
  dark:after:rounded-lg
  dark:before:hidden
  dark:bg-[--btn-bg]
  dark:border-white/5
  dark:text-white
  data-[active]:[--btn-icon:theme(colors.zinc.300)]
  data-[disabled]:opacity-50
  data-[focus]:outline
  data-[focus]:outline-2
  data-[focus]:outline-blue-500
  data-[focus]:outline-offset-2
  data-[hover]:[--btn-icon:theme(colors.zinc.300)]
  focus:outline-none
  font-semibold
  forced-colors:[--btn-icon:ButtonText]
  forced-colors:data-[hover]:[--btn-icon:ButtonText]
  gap-x-2
  inline-flex
  isolate
  items-center
  justify-center
  px-[calc(theme(spacing[3.5])-1px)]
  py-[calc(theme(spacing[2.5])-1px)]
  relative
  rounded-lg
  sm:px-[calc(theme(spacing.3)-1px)]
  sm:py-[calc(theme(spacing[1.5])-1px)]
  sm:text-sm/6
  text-base/6
  text-white"> Button 
</button>

Given this miserable scenario, the introduction of CSS proper would see the web development community, the world over, collectively drowning in their own highly caffeinated excitement wee wees. Within a month, its advent would have spawned a hundred thousand “hey guys!!” Youtube videos, 67 hastily written books, and 31 video courses hosted by creepy, immaculately preened white dudes. All you’d hear about day in and day out would be “the selector engine”, “separations of concern”, and “the cascade” and instead of complaining about CSS being difficult, everyone would be scrambling to master it before their peers. CSS would be embraced and loved because it would be new.

But CSS isn’t new, it’s only good. And in this backwards, bullshit-optimized economy of garbage and nonsense, good isn’t bad enough.


If you enjoyed this diatribe, you may like my videos and there’s a small chance you may be interested in some web themed apparel.