# Four Text Levels, Not Two

**SHOULD** · **ID:** `design-interface-text-hierarchy-levels` · **Category:** design
**Source:** [interface-design](https://github.com/Dammyjay93/interface-design)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/design-interface-text-hierarchy-levels

> SHOULD: Define four text levels as tokens and use all four — primary (body, highest contrast), secondary (supporting copy), tertiary (metadata: timestamps, field labels, counts), muted (disabled/placeholder, lowest contrast). Two levels ("text" and "gray text") collapse every secondary role onto one step, so nothing recedes. Weight and colour carry more hierarchy than size — a single 14px size can hold three tiers.

Build four text levels — primary, secondary, tertiary, muted — and use all four, instead of shipping "text" and "gray text"

The four levels have fixed jobs: primary is default body text at the highest contrast; secondary is supporting copy, slightly muted; tertiary is metadata — timestamps, field labels, counts; muted is disabled and placeholder text, the lowest contrast in the system. Ship only two and every secondary role collapses onto one step: the field label, the supporting sentence, the timestamp and the greyed-out placeholder all shout at the same volume, so nothing recedes and the eye has nowhere to go. The fix is not a new font size — the Apple/Linear move is that weight and colour carry more hierarchy than size, so a single 14px size can hold three tiers cleanly. Define the four as tokens once and bind every text role to one of them, rather than reaching for whichever grey is nearest.

## Bad — do not do this

`design-interface-text-hierarchy-levels-bad`

```tsx
/**
 * Bad: two text levels — "text" and "gray text". The card title, the field labels,
 * the supporting copy, the timestamp and the placeholder all land on one of two steps,
 * so nothing recedes and nothing leads. The hierarchy is flat.
 */
export function InterfaceTextHierarchyLevelsBad() {
  return (
    <div className="w-full max-w-sm space-y-3">
      <p className="text-xs text-muted-foreground">Two levels: primary + one gray</p>

      <div className="rounded-xl border border-border bg-card p-4">
        {/* Level 1 */}
        <h3 className="text-sm font-medium text-foreground">Pull request #482</h3>
        {/* Level 2 — and every remaining role is also level 2 */}
        <p className="mt-1 text-sm text-muted-foreground">Merge the token refactor into main</p>

        <dl className="mt-3 space-y-1">
          <div className="flex justify-between">
            <dt className="text-sm text-muted-foreground">Author</dt>
            <dd className="text-sm text-foreground">ada</dd>
          </div>
          <div className="flex justify-between">
            <dt className="text-sm text-muted-foreground">Opened</dt>
            {/* Metadata sits on the exact same step as the supporting copy above */}
            <dd className="text-sm text-muted-foreground">3 days ago</dd>
          </div>
        </dl>

        <div className="mt-3 rounded-md border border-border px-3 py-2">
          {/* Placeholder — also the same gray */}
          <span className="text-sm text-muted-foreground">Leave a comment&hellip;</span>
        </div>
      </div>

      <p className="text-xs text-error">
        The timestamp, the field labels, the supporting sentence and the disabled placeholder are
        the same gray, so they compete instead of stacking. Only two steps exist, so the eye has
        exactly one decision to make and every secondary role shouts at the same volume.
      </p>
    </div>
  );
}
```

## Good — do this

`design-interface-text-hierarchy-levels-good`

```tsx
/**
 * Good: four levels, used consistently.
 *
 *   primary   — default text, highest contrast
 *   secondary — supporting text, slightly muted
 *   tertiary  — metadata, timestamps, less important
 *   muted     — disabled, placeholder, lowest contrast
 *
 * These are named once and reused, so every role lands on a deliberate step rather
 * than on whichever gray was closest to hand.
 */
const TEXT = {
  primary: 'text-foreground',
  secondary: 'text-neutral-700 dark:text-neutral-300',
  tertiary: 'text-neutral-500 dark:text-neutral-400',
  muted: 'text-neutral-400 dark:text-neutral-600',
} as const;

export function InterfaceTextHierarchyLevelsGood() {
  return (
    <div className="w-full max-w-sm space-y-3">
      <p className="text-xs text-muted-foreground">Four levels: primary / secondary / tertiary / muted</p>

      <div className="rounded-xl border border-border bg-card p-4">
        <h3 className={`text-sm font-medium ${TEXT.primary}`}>Pull request #482</h3>
        <p className={`mt-1 text-sm ${TEXT.secondary}`}>Merge the token refactor into main</p>

        <dl className="mt-3 space-y-1">
          <div className="flex justify-between">
            <dt className={`text-sm ${TEXT.tertiary}`}>Author</dt>
            <dd className={`text-sm ${TEXT.primary}`}>ada</dd>
          </div>
          <div className="flex justify-between">
            <dt className={`text-sm ${TEXT.tertiary}`}>Opened</dt>
            {/* Metadata drops a full step below the supporting copy */}
            <dd className={`text-sm ${TEXT.tertiary}`}>3 days ago</dd>
          </div>
        </dl>

        <div className="mt-3 rounded-md border border-border px-3 py-2">
          {/* Placeholder lives on the lowest step, so it reads as "not yet content" */}
          <span className={`text-sm ${TEXT.muted}`}>Leave a comment&hellip;</span>
        </div>
      </div>

      <div className="space-y-1 rounded-md border border-border bg-muted p-3">
        <p className={`text-xs ${TEXT.primary}`}>Primary &mdash; default text</p>
        <p className={`text-xs ${TEXT.secondary}`}>Secondary &mdash; supporting text</p>
        <p className={`text-xs ${TEXT.tertiary}`}>Tertiary &mdash; metadata, timestamps</p>
        <p className={`text-xs ${TEXT.muted}`}>Muted &mdash; disabled, placeholder</p>
      </div>

      <p className="text-xs text-success">
        Four distinct steps, each with one job. The title leads, the description supports, the
        metadata recedes, and the placeholder is quietly the quietest thing on the card &mdash;
        without changing a single font size.
      </p>
    </div>
  );
}
```

## References

- [interface-design (Damola Akinleye)](https://github.com/Dammyjay93/interface-design)
- [NN/g: Visual Hierarchy](https://www.nngroup.com/articles/visual-hierarchy-ux-definition/)
