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.

Five new primitives. One type scale. One spacing scale. One rhythm.

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 the drmg-design-curator agent (.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.

Primitivesrc/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.

Compositesrc/components/composite/{domain}/ — business-meaningful components used on two or more pages. Examples: PRDCard, VentureCard, PromptDeck. Composes primitives; never defines its own atoms.

Page-privatesrc/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:

  1. Will this component appear on more than one page? Yes → composite. No → page-private.
  2. Is it an atomic token with no business context? Yes → primitive.
  3. 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.
  • Lintablenpx @google/design.md lint DESIGN.md validates schema + contrast in seconds. Drift is detectable.
  • Diffablenpx @google/design.md diff a.md b.md surfaces 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 biblesrc/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-workchartdesign.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-decksrc/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):

  1. Overview (Brand & Style)
  2. Colors
  3. Typography
  4. Layout (Layout & Spacing)
  5. Elevation & Depth
  6. Shapes
  7. Components
  8. 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.

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

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>
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.


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):

ForegroundWhitegray-50gray-100gray-200bg-ink
--color-brand #b91c1c6.47:16.19:15.88:15.23:12.76:1
--color-brand-dark8.31:17.95:17.55:16.71:12.15:1
--color-brand-light4.83:14.62:14.393.903.70:1
--color-brand-on-dark3.76:13.60:13.423.044.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

RoleMinimum
Body text16px (text-body-base)
Small caps label12px with ≥0.08em letter-spacing
Captions / meta14px (text-body-sm)
DisplayPer 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-eval skill — 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/ includes G-legibility-passes as 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/:

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>

Change Log

DateChangeReason
2026-05-18Locked three-layer taxonomy (primitive / composite / page-private) + co-location ruleWik decision — fixes flat 22-folder structure that violated naming-standards.mdx React Components section

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?