Tags: web-dev concept

Utility-First vs Semantic CSS

Date: 2026-08-19


Both sides are arguing about where the abstraction lives, not about class names. Semantic CSS names the thing and describes it elsewhere; utility CSS names the properties and composes them at the point of use. The argument is really about which one drifts worse over three years.


Semantic CSS gives an element a class describing what it is, and a stylesheet defines what that looks like. Utility-first CSS gives an element a set of single-purpose classes, each mapping to one declaration.

Semantic — the markup names the thing, a stylesheet says what it looks like:

<article class="product-card">…</article>
.product-card { display: flex; gap: 1rem; padding: 1rem;
                border-radius: 8px; background: var(--surface); }

Utility-first — the same declarations, composed at the call site. There is no second file:

<article class="flex gap-4 p-4 rounded-lg bg-surface">…</article>

Same rendered output. The difference is where you look to answer a question, and each direction wins a different question.

The case for utilities, stated properly

  • No naming. Naming things you don’t reuse is real cost with no return, and .product-card-inner-wrapper-2 is the observable end state of trying
  • Deletion is safe. Remove the markup, remove the styles. Semantic stylesheets accumulate rules nobody dares delete because nobody can prove what uses them
  • The stylesheet stops growing. Utility CSS is bounded by the design system’s token set; semantic CSS grows with every component ever written
  • Constraint by construction. p-4 picks from a scale. A free-form padding: 14px doesn’t, and that’s how spacing systems erode — Design Tokens and Spacing Systems
  • Locality. The styles are where the markup is; no jumping between files to answer “why is this element blue”

The case for semantic CSS, stated properly

  • The markup says what things are. product-card survives a redesign; flex gap-4 p-4 describes this month’s design and has to be rewritten when it changes
  • Change once, not everywhere. Altering every card means editing one rule, versus a find-and-replace across templates — real leverage exactly when a design system changes
  • Media queries and states read better in CSS than as a stack of prefixed variants on one element
  • Markup stays readable. A component with eighteen classes on a div is genuinely harder to scan, and it’s the honest complaint

Where the argument dissolves

Components make most of it moot. In any framework, the reuse unit is the component, not the class. Write utilities inside <ProductCard>, use <ProductCard> fifty times, and you have single-point-of-change and no naming problem — because the component name is the semantic name.

utilities + components         the class soup exists once, in one file
                               the call sites say <ProductCard />
                               changing every card = editing one component

Which means the real question is whether your reuse unit is a component or a class. If it’s a component, the utility objection about repetition mostly evaporates. If you’re writing templates without a component layer — a CMS theme, email HTML, server-rendered partials — the objection stands, because there is nothing to hoist the repetition into.

What each actually fails at

Failure mode
Utility-firstClass soup in codebases with no component layer · long conditional class strings that are unreadable and easy to get wrong · a build step in the critical path · arbitrary-value escapes (p-[13px]) quietly reintroducing the free-form problem the scale was meant to solve
SemanticUnbounded stylesheet growth · [[The Cascade and Specificity

Both failure modes are drift, arriving from opposite directions: utilities drift when the escape hatches get used, semantics drift when nobody can delete anything.

Filing this honestly

There’s no general answer, and the strongest predictor of success is consistency rather than choice — a codebase half-committed to each has both failure modes at once. The decision that actually matters underneath either is whether the values come from a token set at all, which is Design Tokens in Practice and applies identically to both.