Skip to main content

Design System

One sentence per component. If a component needs more than one sentence, it is doing more than one job.

This is the schema for src/components/design-system/. Five new primitives. One type scale. One spacing scale. One rhythm.

The Cognitive Contract

Every component is an anchor in working memory. Once the reader has seen it on page A, recognition on page B is free — zero cognitive cost. Inconsistency breaks the contract: each unfamiliar pattern is a new thing to learn.

Miller minus two — the ceiling is five items before the reader needs a click. No list, grid, or stat row in src/pages/ should exceed five items without a disclosure. TightFive enforces this at render time.

Feynman test — each component below is described in one sentence. If you can't write one sentence for a new component, the component is not ready to ship.

Lindy dimension — tokens that survive page A, page B, page C compound into muscle memory. Tokens that differ page to page pay the learning tax three times.

Type Scale

Added to tailwind.config.js. Covers headings, body, and the eyebrow atom.

TokenSizeLine HeightLetter SpacingUse
text-display-13rem1.1−0.02emh1 hero
text-display-22.25rem1.15−0.015emh2 section
text-display-31.5rem1.25−0.01emh3 subsection
text-body-lg1.125rem1.6lede paragraph
text-body-base1rem1.6default body
text-body-sm0.875rem1.5caption, meta
text-eyebrow0.75rem10.15emmono uppercase label

Pair text-display-* with font-bold. Pair text-eyebrow with font-mono uppercase.

Spacing Scale

Three rhythm tokens. Section spacing (sm/md/lg/xl) handles the gap between sections. Rhythm tokens handle the gap inside a section.

TokenValueUse
space-y-rhythm-tight12pxItems in the same idea (label + value)
space-y-rhythm-base24pxSibling blocks inside a section
space-y-rhythm-loose48pxDistinct concepts inside one section

The same tokens are used by the Rhythm primitive and Card density prop. One scale, multiple surfaces.

Five Primitives

Eyebrow

Mono uppercase label. The atom that signals what's about to be said.

import { Eyebrow } from "@site/src/components/design-system";

<Eyebrow>Section label</Eyebrow>
<Eyebrow tone="subtle">When</Eyebrow>
<Eyebrow dark>Inverted label</Eyebrow>
PropValuesDefault
toneaccent, subtleaccent
darkbooleanfalse
asp, span, h3, h4, divp

accent = brand red / brand-on-dark. subtle = ink-subtle / chalk-subtle. Use subtle for structural labels (When, So I get, Not). Use accent for section openers.

Consolidates: hand-coded <span className="text-xs font-mono text-brand uppercase"> patterns + SectionLabel (kept as a re-export with tone="subtle").


TightFive

Show five things, fold the rest behind a click.

import { TightFive } from "@site/src/components/design-system";

<TightFive as="grid">
{items.map((item) => (
<ItemCard key={item.id} {...item} />
))}
</TightFive>;
PropValuesDefault
ascol, row, gridcol
gaptight, base, loosebase
moreLabelstring"See more"
lessLabelstring"Show fewer"

Throws a console warning (dev only) when more than 5 children are passed. In production, extras fold behind a mono uppercase disclosure button. Use everywhere a list, grid, or stat row is shown.


Rhythm

Stack children with one of three repo-wide gaps.

import { Rhythm } from "@site/src/components/design-system";

<Rhythm gap="tight">
<Eyebrow>Label</Eyebrow>
<p>Value</p>
</Rhythm>;
PropValuesDefault
gaptight, base, loosebase
asdiv, section, article, ul, ol, header, footerdiv

Replaces space-y-2, space-y-4, space-y-6, space-y-8 ad-hoc classes inside sections. Three values are the maximum — if a fourth is needed, the spacing model is wrong.


Stat

Mono number on top, label below.

import { Stat } from "@site/src/components/design-system";

<Stat value="$25.8K" label="M12 Cumulative" sub="Base case" />
<Stat value="M4" label="Break-even" variant="card" />
PropValuesDefault
variantbare, cardbare
darkbooleanfalse
aligncenter, leftcenter

bare = no background, use inside grids. card = surface card (replaces StatCard). StatCard is kept as a backward-compatible wrapper around Stat variant="card". New code uses Stat directly.


Divider

A line you can label.

import { Divider } from "@site/src/components/design-system";

<Divider />
<Divider caption="Phase 2" />
<Divider caption="or" dark />
PropValuesDefault
captionstring
darkbooleanfalse

Without caption renders as <hr>. With caption renders the label centred across the rule. Use inside a Section to segment without nesting another Section.


SectionHeader

Eyebrow + heading + lede in a fixed rhythm.

Now composes Eyebrow internally. The label prop renders through Eyebrow. Heading size is automatic based on as prop:

asHeading class
h1text-display-1
h2text-display-2 (default)
h3text-display-3

Card (density prop)

New density prop maps onto rhythm tokens for inter-child spacing:

densityMaps to
compactspace-y-rhythm-tight
basespace-y-rhythm-base (default)
spaciousspace-y-rhythm-loose

Chip (variant prop)

Renamed from tone to variant for consistency with Badge, Pill, Callout.


Routing Test

Before writing inline Tailwind inside src/pages/:

PatternUse instead
<span className="text-xs font-mono text-brand uppercase"><Eyebrow>
<div className="space-y-4"><Rhythm gap="tight">
<div className="space-y-8"><Rhythm gap="base">
<p className="text-2xl font-bold font-mono"><Stat value={...} label={...}>
<hr className="border-t border-edge"><Divider>
List of more than 5 items with no disclosure<TightFive>

Context

Questions

How do you decide whether a pattern needs a new primitive or can use an existing one?

  • When does Rhythm become a wrapper for Stat vs using Stat directly in a grid?
  • What is the right primitive when you need both a disclosure and a rhythm container?
  • How do you audit a page for Tight Five compliance without reading every JSX node?
  • When should Card density replace Rhythm, and when should they coexist?