The ramp, the roles, the title sets, the ink ladder, mono, tracking, truncation, and the do-nots. Sources: kit theme/index.css (fonts and base size), the R6 survey of every class the four codebases use (.claude/output/20260816-r1-sitting-1/R6-typography.md, exact counts), the structure note docs/plan/60-typography-structure.md, and the owner's rulings in docs/plan/57-sitting-1-answers.md (D9a, D10a) and docs/plan/59-showcase-feedback-4.md (D3, D4, G1, V1). The rendered companions are the ui page /components/typography (every role and variant on fixture data) and the pattern page /patterns/typography-composites (where two or more roles meet). This doc says why and what not to do, and never repeats what they show.
Audience: an agent (or person) choosing type for a screen, or reviewing one.
§A Principles and mental models#
1. The ramp is three working sizes and two display sizes.
text-xs, text-sm and text-base carry the apps (R6: 111/103/233 in speedway, 162/143/155 in walmart). lg and up appear a handful of times per app, and headings come from PageTitle and Card rather than raw classes. 2xl and 3xl are display sizes for the playground landing and nothing else. A fourth working size is a design decision made in this doc, never a class typed on a page. Never type an arbitrary pixel size (text-[13px]) without a reason written beside it; R6 counted 84 across the tree, 41 in walmart, and each is a size outside the ramp.
2. Roles, not sizes.
Reach for the component that owns the role before the class that approximates it: PageTitle for the page name, Card title and subtitle for a block, Kicker for a group title or a tile label, StatTile for a headline number, Chip for a token, a Button for an action. The ui page's roles table renders each with the classes it applies, so a hand-rolled copy can be spotted by its class string.
3. Mono is for identifiers, never decoration.
Doc 1 §A already says it (01-foundations.md:28). This doc adds the four clauses (§B, "Mono in place"). Inside a button, a count or an id keeps the button's size and inherits its colour with no separate opacity. Inside a code block, mono is the medium and the header title is mono only when it is itself code. Inside an alert, an identifier is the inline code chip, never a mono paragraph, and the alert title is never mono. Inside a chip, the label is sans and only an id, SKU or path is mono. Never use mono to make text look technical.
4. One kicker idiom, one component, never re-typed.
Doc 9 §A5 names the idiom the house section-opener. The kit now ships it as Kicker (packages/ui/docs/kicker.md) in two variants, default (11px, 0.08em, the group title and the eyebrow) and bricked (12px, tracking-wide, the tile label read inside its own frame). The one place the idiom drops to weight 500 is a railed sidebar's group kicker (navGrouping="railed", doc 08 §A10), so a group title recedes behind its rows without leaving the /65 ink floor of §A9; the default flat sidebar keeps 600. Never type uppercase tracking-wide text-xs on a page again; R6 counted about 76 hand-typed copies and 176 uppercase classes in walmart alone, which is how the same label ended up in three recipes on one landing page (doc 59 V1).
5. Four title levels and a label. A page never invents a fifth.
Page title (PageTitle, h1 text-2xl font-bold), display title (the landing's h2 only), section or card title (Card title card-title text-base with its text-sm subtitle, titleDivider when the body is a table), group title (Kicker default) and the tile label (Kicker bricked). The stat label belongs to StatTile and is sentence case. A card's title node may carry a leading icon at size 18 in the primary tone. The icon never sits in a row of its own above the title. Never place a section's action under its title in a row of its own. It lives in the toolbar slot (doc 2 §A10).
6. Weight is hierarchy, not emphasis. Bold in a sentence is a smell. The ramp's weights (400 body, 500 labels, 600 kickers and card titles, 700 page and display titles) already say what is above what. Never bold a phrase inside body text to make it louder.
7. Case is meaning.
Uppercase belongs to the kicker and to nothing else. Sentence case everywhere else, including stat labels and buttons; table headers render as authored, in Title Case, per doc 06 §B (06-tables.md:39, owner rounds 2026-07-10 and 07-16). Never uppercase a button, a table header or a status pill.
8. Colour on text is meaning, and the meaning comes from doc 3.
A tone on text says status or intent (the five semantic tones, docs/design-language/03-status-language.md), never emphasis or decoration. The ui page shows tone-on-text once and points here. A kicker never carries a tone (a coloured label reads as a status). Measured 2026-08-17: in the light theme every one of the five tones on 14px body text sits under AA against a card (warning 2.87, error 3.44, info 3.61, success 3.90, primary 4.20) and all five pass in dark, so tone on text is safe at 16px and up, or where the tone rides a pill or chip that brings its own surface; below that in light it is a token-level call the owner has not made yet. Never colour a paragraph or a title for effect.
9. Gray is hierarchy, and it is a ladder, not a slider.
text-base-content at full ink is titles and values. /70 body prose and the footer. /65 card subtitles, stat labels, kickers, tile labels, hints and help text. /45 a count riding inside a control (it inherits the control's ink, so it is never the only text on a surface). /30 a disabled icon wash. Two rungs the ladder does NOT have for small text: /50 and /60, which measure 3.2 to 4.4 against the light theme's page at 11 to 12px (the wave 2b categorical pass, both themes) and fail AA; they were the kicker, hint and footer inks until 2026-08-17. Never invent /55 because it "looked right", and never drop small text below /65. The ui page renders the ladder as a column with each rung's role.
10. Tracking lives on the kicker and nowhere else.
0.08em on the default kicker and tracking-wide on the bricked one are the only letter-spacing in the system. Body, titles and buttons keep the font's own spacing. Spacing around type comes from the containers (doc 9 §A5's 30/26px section rhythm, Card's padding, the title stack's gap-2), never from margins typed onto a text node.
11. Truncated text always has a way to read the whole value.
truncate pairs with a Tooltip carrying the full text (packages/ui/docs/tooltip.md, "When to reach for it"). line-clamp is for prose that must stop. break-all is for paths (the Markdown inline-code rule). Never truncate a value the reader must be able to read in full without giving them the tooltip.
12. A button never underlines.
At rest or on hover, in any variant. Hover feedback is the shade wash (packages/ui/src/button/button.tsx:154-157). Underline belongs to prose links inside Markdown. A project may rule otherwise for its own inline links, and then it does so everywhere. Never mix the two on one surface (doc 59 D4).
§B Presets#
Each row is the type recipe for a role, and the component that produces it. Use the component. The classes are printed so a hand-rolled copy is recognisable in review.
| Role | Component | Classes today | Notes |
|---|---|---|---|
| Page title | PageTitle (packages/ui/src/page-title/page-title.tsx:86) | h1 text-2xl font-bold, truncating | one per page, first in the composition order (doc 9 §A4) |
| Display title | the landing's LandingSection h2 (apps/playground/src/app/landing-sections.tsx:58) | text-2xl md:text-3xl font-bold tracking-tight under a Kicker | landing only |
| Card or section title | Card (packages/ui/src/card.tsx:114) | h2 card-title text-base; subtitle text-base-content/65 text-sm | header doubles as documentation (doc 5 §A1); titleDivider for a table body |
| Group title | Kicker default | text-[11px] font-semibold tracking-[0.08em] uppercase text-base-content/65 | the level below a card title; as="h3" for the outline |
| Tile label | Kicker variant="bricked" | text-xs font-semibold tracking-wide uppercase text-base-content/65 | a demo or specimen tile's own name; not the Button sense of bricked |
| Stat label and value | StatTile (packages/ui/src/stat-tile/stat-tile.tsx:78,85) | label text-xs font-semibold text-base-content/65, value font-mono text-3xl font-semibold tabular-nums | the value's tone is the tile's tone |
| Body | prose in a Card or Markdown | text-base text-base-content/70 (docs bumped one size, doc 53 G6.9) | max-w-2xl for reading measure on a landing |
| Help text and hints | under a field, a footer, a hint line | text-xs text-base-content/65 | never smaller than xs, never lighter than /65 |
| Mono in place | a count or id in a Button, a CodeBlock title that is code, an inline code chip in an Alert, a Chip value | font-mono, inheriting size and colour from the host | see §A3; the See all count on the landing is the reference case |
| Table header and cell | Table | header as authored (Title Case, doc 06 §B) at text-xs font-medium text-base-content/65 (packages/ui/src/table/table.tsx:157); cells text-sm, ids mono, numerals right-aligned tabular-nums | the pattern page shows the pair |
| Message text | Alert title over body, toast one-liner, EmptyState title over hint | title text-sm font-semibold, body text-sm text-base-content/70, hint text-xs text-base-content/60 | one size step between title and body, never two |
§C Use cases through the apps#
| Case | Where | What it shows |
|---|---|---|
| The eyebrow above a landing section | apps/playground/src/app/landing-sections.tsx (Kicker over the display h2, purpose line under) | the title stack at display scale, gap-2 inside it |
| A demo tile with a bricked label | apps/playground/src/app/landing-strip.tsx (Tile) | a Card with no title, the label as the tile's name, the component link in the toolbar position |
| Group titles inside one card | /components/kicker, "Group titles inside one card" | Kicker as="h3" opening two form groups under one card title |
| A count riding a text button | the landing's "See all 41 pages" | mono, the button's size, the button's colour, no separate opacity (doc 59 C3) |
| The gray ladder in one screen | /components/typography, "The ink ladder" | each rung labelled with its role |
| Composites | /patterns/typography-composites | title stack, label and value pairs, table header versus cell, identity row, message text, prose in a card, the four levels in situ |
| Speedway's five kicker names | speedway app.css (.section-label, .panel-kicker, .rail-label, .field-label, .orch-k), doc 9 §A5 | the idiom before the primitive; the app adopts Kicker on its next deliberate kit upgrade |
| Walmart's hand-typed uppercase | walmart-mvp/frontend/src/pages/Admin.tsx, NewCatalog.tsx, WalmartExplorer.tsx, Upload.tsx, WalmartErrors.tsx (R6 survey) | 176 uppercase and 198 tracking- classes for one idiom; the reason §A4 exists |