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-2is 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-4picks from a scale. A free-formpadding: 14pxdoesn’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-cardsurvives a redesign;flex gap-4 p-4describes 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-first | Class 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 |
| Semantic | Unbounded 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.