Design System
One sentence per component. If a component needs more than one sentence, it is doing more than one job.
Five new primitives. One type scale. One spacing scale. One rhythm.
- Implement into TailwindCSS Config
Owner. This page is the public schema in the design authority chain. The chain runs public schema → public hub (Product Design) → operational bible (
src/components/DESIGN.md) → runtime tokens (tailwind.config.js). Drift across the chain is owned by thedrmg-design-curatoragent (.claude/agents/drmg-design-curator.md). When this page changes, the bible and the config change in the same PR — or invoke the curator to detect and propose the drift fixes.
Three-Layer Taxonomy
Every component lives in exactly one layer. The layer is determined by scope of use, not by complexity.
Primitive — src/components/design-system/{category}/ — atomic building blocks with no business meaning. Shared across every surface. The five categories are layout, typography, surface, input, data, and media. Full component list in §Five Primitives.
Composite — src/components/composite/{domain}/ — business-meaningful components used on two or more pages. Examples: PRDCard, VentureCard, PromptDeck. Composes primitives; never defines its own atoms.
Page-private — src/pages/{path}/_components/ — components used on exactly one page, living beside that page. The _ prefix skips the folder as a Docusaurus route. When the page is deleted, the component goes with it.
Co-location decision:
- Will this component appear on more than one page? Yes → composite. No → page-private.
- Is it an atomic token with no business context? Yes → primitive.
- Does a page-private component get imported by a second page? Move it to composite in the same PR.
Worked example: a hero section built only for /vessel/ lives at src/pages/vessel/_components/Hero.tsx. If a second page imports it, it moves to src/components/composite/vessel/Hero.tsx before that PR merges.
The DESIGN.md Standard
We adopt the Google Labs design.md format — a plain-text, two-part document (YAML front matter + Markdown body) that is the canonical design memory for AI agents. The format choice traces to Meng To's "AI Design Workflow That Doesn't Ship Slop": proprietary Figma/Framer pixels cannot transfer to an agent; a markdown file on disk can. Local-first beats cloud-first because the moat is the file.
Why this standard:
- Portable — the same file describes the system to Claude, Codex, Cursor, Lovable, Stitch, v0. No re-briefing per tool.
- Lintable —
npx @google/design.md lint DESIGN.mdvalidates schema + contrast in seconds. Drift is detectable. - Diffable —
npx @google/design.md diff a.md b.mdsurfaces token regressions between versions. - Composable — the
{path.to.token}reference syntax lets components inherit from primitives without duplicating values.
Four scopes, four files. One spec.
System bible — src/components/DESIGN.md — the canonical design memory for everything in src/components/. Tokens align with tailwind.config.js (the runtime source of truth). Every primitive is documented in §Components. Every PR touching src/components/ runs the lint gate against this file.
Per-venture — .invisible/ventures/{slug}/design.md — DNA for pirates jumping ship. One recipe per venture, because the venture IS the brand. Required by the Design DNA Gate before the PR opens. Paired with src/components/{slug}/design-dna.tsx (runtime instrument).
Per-workchart — design.md scoped to the workchart — DNA for small-org owners reinventing existing businesses. One recipe per workchart, inherited by every journey token. Paired with src/components/journeys/shared/design-dna.tsx.
Per-deck — src/components/{deck}/design.md — DNA for load-bearing primitives that carry their own design contract. Today the prompt-deck is the only deck; future load-bearing primitives that ship a scoreGenericTest() runtime get one. Paired with src/components/{deck}/design-dna.tsx.
Section order (every scope, exact sequence):
- Overview (Brand & Style)
- Colors
- Typography
- Layout (Layout & Spacing)
- Elevation & Depth
- Shapes
- Components
- Do's and Don'ts
Sections may be omitted, but those present appear in this order. Duplicate section headings are a lint error.
Token schema (YAML front matter):
---
version: alpha
name: <design-system-name>
description: <optional>
colors:
<token-name>: "#hex"
typography:
<token-name>:
fontFamily: <string>
fontSize: <Dimension>
fontWeight: <number>
lineHeight: <Dimension | number>
letterSpacing: <Dimension>
spacing:
<scale-level>: <Dimension | number>
rounded:
<scale-level>: <Dimension>
components:
<component-name>:
backgroundColor: "{colors.primary}"
textColor: "{colors.neutral}"
typography: "{typography.body-md}"
---
Tokens are the normative values. Prose explains how to apply them. Token references use {path.to.token} and must resolve to primitive values (except inside components, where composite references like {typography.label-md} are permitted).
Promotion rule: when a token added to a per-scope DESIGN.md proves load-bearing across two or more scopes, it promotes into the system bible in the same PR. When a system-bible token is overridden by every scope, it deprecates from the system bible.
Lint gate (PR-blocking):
npx @google/design.md lint src/components/DESIGN.md
npx @google/design.md lint .invisible/ventures/{slug}/design.md
A PR that modifies any DESIGN.md must show clean lint output (zero errors, warnings acknowledged). The script lives in .husky/pre-push after Gate 4.6 (TypeScript type check).
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
Primitives live under src/components/design-system/{category}/ — one category subfolder per concern (layout, typography, surface, input, data, media). The five primitives below are the current canonical set.
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.
Legibility
Text and words are design. If a reader cannot read it, it is not designed — it is decorated.
Every rendered page passes a five-gate legibility eval before ship. Three gates compute deterministically (contrast, size, mobile reflow). Two require judgment (color role, hierarchy scan). All five are mandatory.
G1 — Contrast (computed)
WCAG 2.1 contrast ratio for every foreground/background pair on the page. Compute via sRGB → relative luminance → ratio formula. Do not estimate.
Threshold: AA normal text 4.5:1, AA large text 3.0:1, AAA normal 7.0:1, AAA large 4.5:1. AA is the floor; AAA is the target for body text.
Brand-red contrast cache (computed 2026-05-16, recompute when CSS tokens change):
| Foreground | White | gray-50 | gray-100 | gray-200 | bg-ink |
|---|---|---|---|---|---|
--color-brand #b91c1c | 6.47:1 | 6.19:1 | 5.88:1 | 5.23:1 | 2.76:1 |
--color-brand-dark | 8.31:1 | 7.95:1 | 7.55:1 | 6.71:1 | 2.15:1 |
--color-brand-light | 4.83:1 | 4.62:1 | 4.39 | 3.90 | 3.70:1 |
--color-brand-on-dark | 3.76:1 | 3.60:1 | 3.42 | 3.04 | 4.74:1 |
Bold cells fail AA normal text (need 4.5:1). --color-brand-light (#dc2626) and --color-brand-on-dark (#ef4444) on any gray panel = no body-text use. Only --color-brand-dark clears AAA on light surfaces. On bg-ink only --color-brand-on-dark clears AA.
G2 — Type size
| Role | Minimum |
|---|---|
| Body text | 16px (text-body-base) |
| Small caps label | 12px with ≥0.08em letter-spacing |
| Captions / meta | 14px (text-body-sm) |
| Display | Per type scale, never below text-display-3 (24px) |
Small caps below 12px is forbidden regardless of contrast — letter shape compression compounds with size to destroy legibility.
G3 — Color role (one role per color)
A color does one job. Brand red = alarm (kill switches, named-problem callouts, the bad metric). Neutral charcoal = structure (eyebrows, labels, headings). Status green/amber/red = lifecycle badges only. Any color appearing in three or more semantic roles is a failure — the eye loses hierarchy.
Audit: list every place a brand accent appears. If the list groups into one role, pass. If it spans multiple unrelated roles, refactor — most uses move to neutral, the brand color retains the alarm role only.
G4 — Mobile reflow
WCAG 2.1 SC 1.4.10 — content reflows at 320px without two-dimensional scrolling. Test in a 320px viewport; any horizontal scroll bar fails the gate. Tables convert via the Inner/Outer Boundary mobile-first rules; long words break or wrap.
G5 — Hierarchy scan
The eye must read eyebrow → headline → body in under 3 seconds on first load. If a tired reader cannot identify the section topic and the lede on first scan, hierarchy is broken — usually because every accent competes for the same attention budget (G3 failure). Fix G3, retest G5.
Running the eval
Three options, in increasing depth:
- Inline review — agent reads the file, lists every color/size used, computes contrast for each pair, prints a pass/fail table.
legibility-evalskill — runs all five gates against a live URL or rendered MDX file, outputs structured report.- Workchart render-task gate — every task that produces a rendered page in
src/pages/includesG-legibility-passesas a machine gate; output cannot ship until all five pass.
A page that fails any gate is a blueprint failure, not an output failure — patch the producing task per .claude/rules/blueprint-patch-contract.md.
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> |
Change Log
| Date | Change | Reason |
|---|---|---|
| 2026-05-18 | Locked three-layer taxonomy (primitive / composite / page-private) + co-location rule | Wik decision — fixes flat 22-folder structure that violated naming-standards.mdx React Components section |
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?