---
title: 'The Utility Class Approach'
source: 'https://academia.sh/en/courses/layout-and-responsive-design/utility-class-approach'
course: 'Layout Systems and Responsive Design'
language: en
updated: '2026-08-17T18:09:30+00:00'
license: 'CC BY-SA 4.0'
---

# The Utility Class Approach

Constraint-based style systems; single-declaration classes taking a value from a closed scale, the set growing independent of component count, repetition moving from the stylesheet into the document, and where the approach breaks.

Every approach up to this point has shared the same assumption: each component has its own
rules, and the stylesheet grows component by component. There is an approach that argues the
opposite.

The idea is this: no component-specific rule is ever written. The document is built out of
predefined, single-declaration classes. Box design happens not in the stylesheet, but in the
document.

## The Constraint Idea

A **utility class** is a class that writes a single declaration and takes its value from a
closed scale. Its name announces what it does: `p-1` gives a padding value, `gap-0` gives a
gap value, `bg-surface` gives a surface color.

The approach's name evokes brevity, but its real justification is not brevity, it is
**constraint**. The scale is closed: if there are five values for padding, there is no way to
write a sixth. Design consistency stops being a review question; the system itself makes an
arbitrary value impossible.

The second consequence concerns naming. In the utility class approach, no new name is
**generated**; the names in the set are known from the start. The previous three lessons'
problems — name collision, ownership, deletability — do not disappear, they become moot: the
class belongs to no one.

## The Set's Bounded Growth

```js
// utility.mjs — the utility class set's bounded growth, compared against component notation
// Utility class: a class writing a single declaration, taking its value from a closed scale.

// Closed scale: the number of selectable values for each property is fixed.
const SCALE = {
  "padding":       5,   // --spacing-0 .. --spacing-4
  "margin-block":  5,
  "gap":           5,
  "font-size":     6,
  "color":         8,
  "background":    8,
  "border-radius": 3,
  "display":       4,
};
const PREFIXES = ["", "hover:", "focus:", "wide:"];   // wide: wide-screen breakpoint

const utilityCount = Object.values(SCALE).reduce((t, n) => t + n, 0) * PREFIXES.length;
const UTILITY_BYTES = 30;                          // example: ".p-3{padding:var(--spacing-3)}"
const utilityTotal = utilityCount * UTILITY_BYTES;

console.log("--- utility class set (independent of component count) ---");
console.log(`  scale: ${Object.keys(SCALE).length} properties, ${Object.values(SCALE).reduce((t, n) => t + n, 0)} values total`);
console.log(`  prefix count: ${PREFIXES.length} (${PREFIXES.map((p) => p || "(no prefix)").join(", ")})`);
console.log(`  generated class count: ${utilityCount}`);
console.log(`  approximate size: ${utilityTotal} bytes`);

// Component notation: every new component adds new rules and new bytes.
const COMPONENT_RULES = 5;                           // average rules per component
const RULE_BYTES = 62;                               // average selector + declaration body

console.log("\n--- size of the two notations by component count ---");
console.log("components".padStart(10) + "component notation".padStart(20) + "utility notation".padStart(19) + "  smaller");
for (const n of [5, 10, 20, 40, 60, 80, 120]) {
  const componentTotal = n * COMPONENT_RULES * RULE_BYTES;
  console.log(
    String(n).padStart(10) +
    `${componentTotal} bytes`.padStart(20) +
    `${utilityTotal} bytes`.padStart(19) +
    "  " + (componentTotal < utilityTotal ? "component notation" : "utility notation"),
  );
}
const threshold = Math.ceil(utilityTotal / (COMPONENT_RULES * RULE_BYTES));
console.log(`  threshold: past ${threshold} components, the utility set stays smaller`);

// --- how many times the same declaration repeats across components ---
const COMPONENTS = {
  "measurement-card": ["padding: var(--spacing-1)", "border-radius: 4px", "display: grid", "gap: var(--spacing-0)"],
  "sidebar-box":        ["padding: var(--spacing-1)", "border-radius: 4px", "display: grid"],
  "masthead":         ["padding: var(--spacing-1)", "display: flex", "gap: var(--spacing-0)"],
  "measurement-filter": ["display: flex", "gap: var(--spacing-0)", "padding: var(--spacing-0)"],
};
const counter = new Map();
for (const declarations of Object.values(COMPONENTS))
  for (const d of declarations) counter.set(d, (counter.get(d) ?? 0) + 1);

console.log("\n--- declarations repeating across components ---");
const totalDeclarations = [...counter.values()].reduce((t, n) => t + n, 0);
for (const [d, n] of [...counter.entries()].sort((x, y) => y[1] - x[1]))
  console.log(`  ${d.padEnd(30)} in ${n} components`);
console.log(`  total declarations: ${totalDeclarations}, distinct: ${counter.size}, repeated: ${totalDeclarations - counter.size}`);

// --- length of the class list in the document ---
console.log("\n--- two notations of the same box ---");
const cardUtility = "grid gap-0 p-1 br-1 bg-surface";
const cardComponent = "measurement-card";
console.log(`  utility notation  : class="${cardUtility}"   (${cardUtility.length} characters, ${cardUtility.split(" ").length} classes)`);
console.log(`  component notation: class="${cardComponent}"   (${cardComponent.length} characters, 1 class)`);
console.log(`  if the card appears 12 times in the document, difference: ${(cardUtility.length - cardComponent.length) * 12} characters`);
```

```
--- utility class set (independent of component count) ---
  scale: 8 properties, 44 values total
  prefix count: 4 ((no prefix), hover:, focus:, wide:)
  generated class count: 176
  approximate size: 5280 bytes

--- size of the two notations by component count ---
components  component notation   utility notation  smaller
         5          1550 bytes         5280 bytes  component notation
        10          3100 bytes         5280 bytes  component notation
        20          6200 bytes         5280 bytes  utility notation
        40         12400 bytes         5280 bytes  utility notation
        60         18600 bytes         5280 bytes  utility notation
        80         24800 bytes         5280 bytes  utility notation
       120         37200 bytes         5280 bytes  utility notation
  threshold: past 18 components, the utility set stays smaller

--- declarations repeating across components ---
  padding: var(--spacing-1)      in 3 components
  gap: var(--spacing-0)          in 3 components
  border-radius: 4px             in 2 components
  display: grid                  in 2 components
  display: flex                  in 2 components
  padding: var(--spacing-0)      in 1 components
  total declarations: 13, distinct: 6, repeated: 7

--- two notations of the same box ---
  utility notation  : class="grid gap-0 p-1 br-1 bg-surface"   (30 characters, 5 classes)
  component notation: class="measurement-card"   (16 characters, 1 class)
  if the card appears 12 times in the document, difference: 168 characters
```

The first two blocks compare two growth curves. The utility set is **constant**: eight
properties, forty-four values, and four prefixes produce 176 classes, and this number is
unaffected by how many components get written. Component notation, on the other hand, grows
linearly.

The model's threshold comes out at 18 components. This number is not an exact value; it
depends on scale width, prefix count, and rules per component. What it gives is not the
threshold itself but **the shape of the two curves**: one constant, one growing.

The third block counts repetition. Four components carry thirteen declarations, but only six
of them are distinct; seven declarations repeat. Utility classes turn exactly these six
declarations into single rules.

## Repetition Does Not Disappear, It Moves

The fourth block shows the approach's cost. The same box is 16 characters in component
notation, 30 characters in utility notation; when the card appears 12 times in the document,
the difference comes to 168 characters.

Repetition removed from the stylesheet has moved into the document. When totaling bytes, this
side has to be counted too; the two files ship together.

But the two repetitions are not of the same kind. Repetition in a stylesheet is managed **by
hand**: changing one value requires finding four rules. Repetition in the document is usually
inside a template or component abstraction; the card is written once and generated twelve
times. The utility class approach therefore **assumes** a template-producing layer; in plainly
written documents, the repetition really is managed by hand.

## State and Breakpoint Prefixes

Since a utility class writes a single declaration, there is no direct place for pseudo-classes
and media queries. The fix is to write the condition into the **name**:

```css
.p-1        { padding: var(--spacing-1); }
.hover\:p-2:hover { padding: var(--spacing-2); }

@media (min-width: 48rem) {
  .wide\:p-2 { padding: var(--spacing-2); }
}
```

The colon inside the name is escaped in the selector with a backslash; the document writes
`hover:p-2`, and the selector matches it as `.hover\:p-2`.

The cost of this is that the set grows by a multiple. In the output's first block, four
prefixes bring forty-four values up to 176 classes. Every new state and every new breakpoint
grows the set by another factor; pruning the generated set down to only the classes that
appear in the document is, for this reason, an inseparable part of the approach — and the
warning from the third lesson applies here too: names must appear as full literal text in the
source.

## Where It Breaks

In three places. First, when a value outside the scale is needed: a specific aspect ratio for
an image, or a measure coming from content, is not in the closed scale, and a one-off rule
needs to be written.

Second is readability. A long class list says how the box **looks**, not what it **is**. A
document writing `class="measurement-card"` explains itself; a list of ten classes does not.

Third is cases needing a complex selector. Style based on a sibling, style based on a
descendant, or style depending on an ancestor's state does not fit into single-declaration
classes.

## Mixed Use

The two approaches do not exclude each other, and in practice the boundary is drawn by this
criterion: **if the structure does not repeat, a utility class; if it repeats, a component
class.**

Generating a name to adjust the padding of a section that appears once in the page is
unnecessary; copying a twelve-times-repeated card's five declarations to twelve places is
unnecessary too. The scale is read from the same custom properties in both notations, so the
two layers share the same design constraint.

## In the Station Page

```css
/* utilities.css — single-declaration classes generated from a closed scale */
.p-0 { padding: var(--spacing-0); }
.p-1 { padding: var(--spacing-1); }
.gap-0 { gap: var(--spacing-0); }
.gap-1 { gap: var(--spacing-1); }
.grid { display: grid; }
.flex { display: flex; }
.bg-surface { background-color: var(--surface); }
.text-muted { color: var(--text-muted); }
```

This set is used for one-off adjustments on the measurement station page: the masthead's
bottom margin, the location section's padding, the source note's color. The measurement card,
on the other hand, stays a component class — it appears twelve times in the page and has its
own variants.

The distinction can be tied to a rule: if a class name appears once in the document, it can be
written with utility classes; if it appears more than twice, it has earned being a component.

## Summary

- A utility class writes a single declaration and takes its value from a closed scale; the
  approach's real justification is not brevity but the constraint that makes an out-of-scale
  value impossible.
- The utility set's size is independent of component count; component notation grows linearly,
  and the two curves cross at a certain component count.
- Repetition does not disappear, it moves from the stylesheet into the document; the approach
  assumes a template-producing layer.
- State and breakpoint prefixes grow the set by a multiple; pruning unused classes is for this
  reason part of the approach.
- If the structure does not repeat, a utility class; if it repeats, a component class; both
  layers share the same custom-property scale.

## Next Step

Five separate notation approaches were taken up in this topic, and all of them answered the
same question from a different place: how style is organized. There is one more question, and
it is asked by the browser — how much does the stylesheet itself delay the page opening, and
how much work is done while matching selectors? The next lesson takes up style's performance
cost, and closes the course.
