The doc set codifies the UI decisions, product patterns, intents, and behaviors proven in Speedway, the flagship app of the shared Versable design language. The ui-kit (packages/ui) is the vocabulary. Speedway's usage is the grammar. Both are captured here so any Versable app (walmart-mvp is the second consumer) can adopt the language without re-deriving it.
Audience: an agent (or person) orienting before any UI work in a Versable app.
Each doc has the same shape:
- §A Principles and mental models: the heuristics, the "how to decide," the why.
- §B Presets and how-to: the concrete patterns to reach for.
- §C Use cases through the app: exhaustive, grouped by surface, with file:line evidence.
The doc map#
| Doc | Trait | Layer |
|---|---|---|
| 01-foundations.md | color, type, spacing, radius, shadow, responsiveness | elementary |
| 02-buttons-and-actions.md | button variants, shades, icons, tooltips, wording | elementary |
| 03-status-language.md | badges, chips, banners, alerts, links | elementary |
| 04-loading-and-states.md | skeletons, spinners, pending bars, empty/error states | elementary |
| 05-cards-and-surfaces.md | cards, framed sections, stat tiles, spacing | composite |
| 06-tables.md | the data-table model: selection, peek, expansion, tints | composite |
| 07-toolbars-and-filters.md | toolbar presets, facets, filter chips, search | composite |
| 08-navigation-and-shell.md | sidebar, topbar, breadcrumbs, page title, shell | composition |
| 09-page-composition.md | widths, padding, section rhythm, page archetypes | composition |
| 10-overlays.md | peek panels, modals, dropdowns, toasts | composition |
| 11-motion.md | animations, transitions, entrance conventions | cross-cutting |
| 12-typography.md | the ramp, roles, title sets, the ink ladder, mono, tracking, truncation | cross-cutting |
Start from what you were asked for, not from a trait#
The table above is organised by trait, which is right for keeping each rule in exactly one place, and wrong for the moment an agent is handed a request. If you were asked to build something, start here instead. Each recipe carries a bundle checklist: every row is a thing the request brings along whether or not anyone said it, with its default, the rule that governs it, and when you may deviate.
Read the recipe's bundle BEFORE the trait docs. The bundle is what makes completeness checkable rather than inferred, and incomplete assembly is the most corrected failure in both apps.
| You were asked for | Recipe | Traits it pulls in |
|---|---|---|
| a list, table, queue, index, "show me all the X" | app-patterns/10-recipe-browsable-list.md | 06 07 04 05 09 02 |
| a record detail, preview, drill-in, "click a row and see it" | app-patterns/11-recipe-record-detail.md | 10 06 04 03 02 |
| a form, settings, or config surface | app-patterns/16-recipe-module-settings-form.md | 02 04 05 10 |
| a dashboard or stat overview | not yet written | 05 09 03 04 |
| an import or upload flow | not yet written | 02 04 10 |
| an admin or diagnostic surface | not yet written | 06 09 |
The unwritten rows are listed on purpose. An agent that finds no recipe for its request should know the gap is known rather than assume the request carries no bundle, and should fall back to the nearest written recipe plus the trait docs its row names.
The core mental models#
1. Three token layers. Only the middle one knows about themes.
Layer 0 is the kit palette: full OKLCH ramps (50 to 950) per hue family, "one ramp, not anchors with filler" (packages/ui/theme/colors.css:14-21). Layer 1 is the kit semantic slots (primary, success, paper, surface, ink, muted, hairline, field dials, status soft/line pairs). These are defined once per theme, in versable-light and versable-dark. Layer 2 is an app's bespoke aliases (Speedway's app.css:11-48): values that point into the kit, with names kept so rules recolor in one place. The hard-won rule: app tokens must read the semantic layer, never the palette layer. Only the semantic layer flips per theme (app.css:50-59 documents the bug that taught this).
2. Identity is not status.
Colors that say what a thing is (part-type, normalize, content, attribute, source, scrape, file, fitment identity tokens) are one vocabulary. Colors that say how it is doing (ok/warn/err/info/review) are another. "WHAT a thing is ... never how it's doing. A status colour can't say this" (colors.css:231-236). Never encode state in an identity color or vice versa.
3. The kit is generic. The app supplies meaning.
Kit components take explicit props (active booleans, NavGroup[], slot nodes) rather than inferring from context. Speedway centralizes meaning in single-source maps. PAGE_META is the canonical example: the sidebar, page titles, and tooltips all read the same entry, so icon and label can never drift (app/lib/page-meta.ts:5-8).
4. Feedback is local and layered, never global dimming.
A pending navigation shows a spinner on the clicked link (delayed 120ms) plus a topbar sweep bar (delayed 90ms). Content is never dimmed: "The sweep bar says 'working'; the route's skeleton says 'here is what is arriving'" (speedway app.css:1093-1102). This generalizes: loading feedback belongs to the thing that is loading.
5. Deliberate absence is a decision, not a gap. Speedway is full of dormant-not-deleted code with dated deprecation comments and revival instructions (workspace switcher, schemas nav, files pages). Owner rulings are recorded verbatim, in place, with dates. When these docs quote a ruling, the quote is binding. The pattern generalizes: record the ruling where the code is, and keep the alternative revivable.
6. Accessibility numbers are measured, not vibes.
Contrast decisions cite measurements: solid warning moved to amber-600 under white ink because "white on amber-500 reads 2.05:1" and the owner wanted white like success (colors.css:168-174). Dark-mode muted text is a 65% mix because it is "the first step clearing 4.5:1 at 12-14px" (speedway app.css:67-69). Copy that discipline, not just the values.
7. Make the wrong state unrepresentable before documenting the ban.
Where a misuse can be made impossible, the API change beats the Banned entry: PageSkeleton requires layout rather than defaulting to a guess about a page nobody looked at (packages/ui/src/page-skeleton/page-skeleton.types.ts:12), the split Button derives its caret's accessible name rather than asking every caller to remember one (packages/ui/src/button/button.tsx:264-265), and presets carry ruled combinations so a caller picks a name instead of reassembling the rule. A Banned entry is the fallback for what the API cannot reach, never the first tool.
Authority and provenance#
- Kit source:
packages/ui/srcandpackages/ui/themein this repo (versable-builder). The owning statement of the kit version and every consumer pin is the first paragraph of the repo'sCLAUDE.md; the numbers below are copied from it on 2026-10-24. - Consumer pins differ, and the difference decides whether a rule in these docs is reachable. The kit is at
0.5.0. Walmart pins^0.2.2inwalmart-mvp/frontend/package.jsonand floats within0.2.x, speedway pins an exact0.2.2, and the forge console pins an exact0.5.0. Under npm semver a caret on a0.xversion never crosses the minor, so walmart and speedway reach nothing added after0.2.xuntil each takes a deliberate upgrade. Check any rule phrased "as of kit 0.N" against the consuming app's pin before you rely on it. - Usage evidence: Speedway at
~/Code/Versable/speedway, as of its 2026-08-06 customer launch. - Owner rulings quoted in these docs are verbatim from code comments. The dated ones (2026-07-07 palette round, 2026-07-17 identity placement, 2026-07-21 doc-29 workspace hiding, 2026-07-29 crumb and counting rules, 2026-08-03 nav and theme gating, 2026-08-05 card-follows-tile) carry the most weight.
- Raw inventories behind these docs:
docs/evidence/20260807-0230-speedway-design-language/(five per-surface registers with file:line for every claim).
The eight laws#
The rules in the trait docs are instances. These eight are the classes behind them: each one was broken somewhere new on every screen swept in the 2026-09 and 2026-10 review rounds, because no single component owns it. Read them before any screen, and when a trait doc is silent, the law still binds.
| Law | What it says | Trait doc that owns it | Measured today by |
|---|---|---|---|
| Fit, then truncate, never clip | Content gets its space first. If it must shrink, it truncates with the full value on hover. Nothing is hard-clipped while free space sits nearby. | 12 typography (truncation), 06 tables | clipped-with-free-space, page-scrolls-sideways |
| Show only what has something to say | A control or value with nothing to report hides or fades: a one-page pager, a "No data", a zero count, an inline clock. | 04 loading and states | nothing yet |
| One signal, said once | No element repeats what another already says: two searches on one table, a dot beside an icon, a count stated twice. | 03 status language, 07 toolbars | two-searches |
| Things beside a label share its line | Icons, counts and tips align to the text they sit next to. | 12 typography | icon-label-misaligned, sibling-scale-mismatch, toolbar-height-mismatch (height only) |
| Machine values stay exact, human text stays human | Ids are never recased. Placeholders never end in an ellipsis. Empty values say N/A. | 12 typography (mono), 14 prop vocabulary | placeholder-ellipsis, empty-cell-without-na |
| Every composite declares its narrow behaviour | Headers, toolbars, tables, footers and the sidebar rail each say what happens at 390px. | 09 page composition | narrow-overflows-viewport, narrow-row-wraps, page-scrolls-sideways |
| Every action answers | A click shows pending, then a result or an error. The pointer appears only where a click does something. | 02 buttons and actions | pointer-not-control, clickable-without-pointer |
| Both themes are first-class | Every colour pair holds contrast in light and dark. | 01 foundations | text-under-aa, run in both themes |
A law with "nothing yet" is checked by eye and by the reading questions in
packages/ui/docs/KIT-STANDARD.md. A component doc names the laws it enforces
in its own "Laws it enforces" list, so a fix to a default can say which class
it closes. The findings behind each law, with the owner's verbatim rulings, are
in docs/plan/85-ui-sweep-pilot-findings.md; the laws were drafted on
2026-10-23 and the owner may reword one.
The ninth and tenth laws#
Two more were ruled after the eight above and bind in the same way. The ninth
was ruled at sitting 1 on 2026-10-23 (D8, "yes, and file both themes"). The
tenth, anatomy, was ruled on 2026-10-24 (E1). They carry the ids L9 and L10
in docs/app-patterns/18-defect-vocabulary.md, which continues plan 86's L1
to L8.
| Law | What it says | Trait doc that owns it | Measured today by |
|---|---|---|---|
| Controls are visible without hover | A control that matters is on screen at rest. Hover may add emphasis, never reveal the control. Touch, keyboard and agent users cannot find a hover-only control. | 02 buttons and actions, 06 tables (row actions) | hover-only-control, hit-target-too-small (notes) |
| The same thing looks the same | One kind of thing has one anatomy wherever it appears: card headers, form labels, surface edges, small-label species and state frames. A second look for the same thing is a defect. | 03 status language (species), 05 cards and surfaces (headers, edges), 04 loading and states (frames), 13 forms and fields (labels) | nothing yet |
The tenth law is the class behind a table not reaching the card edge, a card header unlike its siblings, a card nested in a card, a form with labels at two sizes, and a status pill that looks like an action. When a screen has two looks for one thing, pick the one the trait doc names and delete the other.