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.
| Token | Size | Line Height | Letter Spacing | Use |
|---|---|---|---|---|
text-display-1 | 3rem | 1.1 | −0.02em | h1 hero |
text-display-2 | 2.25rem | 1.15 | −0.015em | h2 section |
text-display-3 | 1.5rem | 1.25 | −0.01em | h3 subsection |
text-body-lg | 1.125rem | 1.6 | — | lede paragraph |
text-body-base | 1rem | 1.6 | — | default body |
text-body-sm | 0.875rem | 1.5 | — | caption, meta |
text-eyebrow | 0.75rem | 1 | 0.15em | mono 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.
| Token | Value | Use |
|---|---|---|
space-y-rhythm-tight | 12px | Items in the same idea (label + value) |
space-y-rhythm-base | 24px | Sibling blocks inside a section |
space-y-rhythm-loose | 48px | Distinct 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>
| Prop | Values | Default |
|---|---|---|
tone | accent, subtle | accent |
dark | boolean | false |
as | p, span, h3, h4, div | p |
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>;
| Prop | Values | Default |
|---|---|---|
as | col, row, grid | col |
gap | tight, base, loose | base |
moreLabel | string | "See more" |
lessLabel | string | "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>;
| Prop | Values | Default |
|---|---|---|
gap | tight, base, loose | base |
as | div, section, article, ul, ol, header, footer | div |
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" />
| Prop | Values | Default |
|---|---|---|
variant | bare, card | bare |
dark | boolean | false |
align | center, left | center |
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 />
| Prop | Values | Default |
|---|---|---|
caption | string | — |
dark | boolean | false |
Without caption renders as <hr>. With caption renders the label centred across the rule. Use inside a Section to segment without nesting another Section.
Promoted Existing Primitives
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:
as | Heading class |
|---|---|
h1 | text-display-1 |
h2 | text-display-2 (default) |
h3 | text-display-3 |
Card (density prop)
New density prop maps onto rhythm tokens for inter-child spacing:
density | Maps to |
|---|---|
compact | space-y-rhythm-tight |
base | space-y-rhythm-base (default) |
spacious | space-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/:
| Pattern | Use 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
- Hacker Laws — Miller's Law and the 7±2 ceiling
- Teacher Archetype — Feynman test and compression
- Knowledge Schema — reuse anchors to reduce cognitive load
- IA Benchmarks — 3–7 items per level, max 3 levels deep
- UI Design Benchmarks — 73-point audit checklist
Questions
How do you decide whether a pattern needs a new primitive or can use an existing one?
- When does
Rhythmbecome a wrapper forStatvs usingStatdirectly 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 densityreplaceRhythm, and when should they coexist?