GUIDE — Component contracts#
Status: law — v1, 2026-08-31 Applies to: every interactive component on a Relay surface — marketing web, web SPA, native app, console, MCP display Parent: GUIDE.interface-principles.md Companions: ../briefs/SPEC.app-components.md (cross-surface inventory — sheet, Approve/Deny, bell, segmented control) · ../briefs/BRIEF.ui-component-kit.md (the relay-app work order) · GUIDE.motion-register.md (motion law) · GUIDE.forms.md (the forms kit)
This is the single table the app and UI teams read — per component: the states it owns, the accessibility behaviour it must carry, and what motion it is permitted. It exists because the contract surface was split three ways: composition law lived here in guidelines/, behaviour contracts lived in briefs/SPEC.app-components.md, and buttons and form fields had no contract rows anywhere. An engineer asking "what states does a button have?" had no page. This is that page.
The table's shape is adapted from the Execution Space estate's interface guide — the operating model, not the visual language. Everything in the cells is Relay's own canon.
How to read the table#
- Copy the specimen's markup, never a screenshot of it. The galleries are the living spec. Every specimen on
../concepts/app-components.htmlends with a copyable class list (.class-list) naming its exact classes and structure — that block is the import statement. Marketing components live inmarketing/web/v36-sharpened/site.css; live form behaviour inmarketing/web/stealth/relay-access.js. - The states column is exhaustive. A state not listed is a state the component does not have. Found a screen that needs one more? That's an amendment to this table, in the same PR.
- The motion column defers to the register. Timing, easing, reduced-motion behaviour and the per-page budget are owned by
GUIDE.motion-register.md; entries here say only what the register permits this component. The register wins on any conflict. - Accessibility is composition, not review (standing rule,
GUIDE.interface-principles.md). The a11y column is part of the component, not a checklist run afterwards. WCAG 2.2 AA is the floor.
The table#
| Component | Copy from | States | A11y behaviour | Motion |
|---|---|---|---|---|
| Button | Marketing: .btn .btn-primary .btn-ghost .btn-lg (marketing/web/v36-sharpened/site.css) · App: the button family on ../concepts/app-components.html | rest · hover (primary: fill mixes toward bright ink; ghost: border + text brighten) · focus-visible (3px outer ring, --rly-accent-ui at reduced alpha) · aria-disabled (dimmed, click intercepted, the reason stated — see GUIDE.forms.md; live in ../concepts/forms-kit-v1.html F06; never a button that plays dead. Known debt: the stealth register's relay-access.js still sets the real disabled attribute on its submit — it predates this standard, per GUIDE.forms.md, and its fix path runs through production first) · pending (label changes to the working verb; never a spinner alone) | Real <button> or <a> — a link styled as a button stays a link. Target ≥44px. Label is the action verb, not "Submit". Filled buttons draw the pair — --rly-accent-fill + --rly-on-accent, never one half; contrast is asserted at token build (≥4.5:1 label-on-fill, both themes). One teal moment per object (DEC-025 L2): a card already showing a teal Code never also gets a teal button. | Never animates, never moves. Colour/border transitions only. No lift, no bounce, no flourish on the element — flourish goes around interactive elements. GUIDE.motion-register.md |
| Form field | .f-field (marketing/web/v36-sharpened/site.css) · live validation lifecycle: marketing/web/stealth/relay-access.js | rest · focus (border to bright ink + soft accent ring) · invalid (set on submit, cleared on input, focus moves to the first invalid field — relay-access.js; the change-for-selects clause is owned by GUIDE.forms.md and lives in ../concepts/forms-kit-v1.html F02 — the stealth register wires no selects) | Visible <label for> on every control — a placeholder is never the label. Helper text is always-visible; aria-describedby wiring is the contract (GUIDE.forms.md, forms-kit F03) — paid 2026-09-24 (site v3.6.4): the v36-sharpened register's invite-code hint now carries id="ic-hint" and its control aria-describedby="ic-hint". autocomplete on name/email. Errors are announced in text, never colour alone, and never discard what someone typed (GUIDE.states-and-language.md). | Never animates, never moves. The focus ring appears instantly; the error state toggles without transition. Ambient flourish may sit around a field only as the register permits, aria-hidden, never on it. GUIDE.motion-register.md |
| Card | App list row: GUIDE.list-surfaces.md (anatomy law, DEC-025-governed) + the card specimens on ../concepts/app-components.html · Marketing: .uc3-card, .platc (site.css) | rest · hover (border brightens — decorative only; information never lives only in hover, rule 6) · focus-within (visible ring when the card's link holds focus) · resolved / selected on app surfaces | A card with one destination is clickable everywhere, not just its link (carried from the Execution Space estate — Erik, 2026-08-28: "this kinda card should always be clickable"). Whole-card <a>, or stretch the inner link over the card; repeated link text ("Open →") gets a per-card aria-label. Whole card is the tap target, ≥44px. Card head, badge rule and the one-teal-moment law stay owned by GUIDE.list-surfaces.md — this row does not restate them. | Hover answer is colour/border only. Any entrance or ambient treatment sits behind/around the card, aria-hidden, within the page budget. GUIDE.motion-register.md |
| Nav | .site-header + .nav (marketing/web/v36-sharpened/site.css) — sticky bar, blur backdrop, burger panel ≤720px | rest · hover (link brightens) · current page · phone (burger opens the stacked panel; body links become full-width rows) | Links are links, in DOM order. Tap targets reach ≥44px by padding, not text height. Nothing in the nav is hover-gated (rule 6 corollary). One <nav> landmark. Paid 2026-09-24 (site v3.6.4): the burger's checkbox is now visually hidden but focusable and named (aria-label="Menu") at ≤720px, its label carries the :focus-visible ring and a 44px target, and Space opens the panel — verified headless by keyboard. The CSS-only pattern stays (no-JS estate); a real <button> with aria-expanded is the upgrade if the nav ever gets script. | Colour transitions only. The bar never animates in or out and never hides on scroll. The panel opens without ceremony. GUIDE.motion-register.md |
| CTA band | .cta (marketing/web/v36-sharpened/site.css) — the same closing structure on every v36-sharpened page: h2 + one paragraph + .btn-primary + mono .cta-note. Since v3.6.2 the close's copy varies per page (pricing closes on its placed keeper, "No tolls in. / No walls out."); the structure does not | Static — the interactive states live on its button. One band per page, always at the page end. | Real <h2>, one primary action, the qualifier in the note line — not in the button label. The band exists for the thumb rule: it repeats the primary action where a reader finishes, instead of sticky-chasing them down the page. | Static by design. The band's headline never animates and the page's one loud moment never lives here — a moving close is a close that competes with its own button. GUIDE.motion-register.md |
| Forms kit (lead capture) | Written contract: GUIDE.forms.md · live wiring: marketing/web/stealth/relay-access.js | The full set — per-field errors, hints, autocomplete, bot-check gating (pending vs never-loaded), consent-locks-submit, the optional ask that never gates (piece 6b — same control, opposite behaviour; the MCP authorization screen's data-capture box is the live case), confirmation screen — is owned by GUIDE.forms.md. This table deliberately does not restate it: one law, one page. | Per GUIDE.forms.md. The confirmation's role="status" + focus move is live in ../concepts/forms-kit-v1.html F07 (tabindex="-1" + .focus() on arrival). The stealth register sets role="status" but makes no focus move — burn-down debt recorded in GUIDE.forms.md: the register predates the standard and its fix path runs through production first. | Fields and submit controls never animate; the confirmation replaces the form as a complete resting frame. GUIDE.motion-register.md |
| Landing-page family | marketing/web/v36-sharpened/index.html and its sibling pages (product/, platform/, pricing/, loop/, use-cases/, security/, register/, login/) | The template is the sequence, read off index.html (v3.6.2 — the 2026-08-31 canon-content rebuild): sticky .site-header → .hero (hook — one h1, primary + ghost action, mono note, the .oath beat line) → the .tools works-with strip → a first full-bleed .moment band carrying the session frame ("Your session doesn't end when the window does") → .keyebrow-headed promise sections (Product loop → .plat Platform strip → the second-brain identity layer → the "Who it's for" segments teaser) → the .moment proof band ("Every hop, on the record") → the .cta band close. Two .moment bands per page is the rebuilt home's shape, not a licence — inner pages carry at most one. A .faq block joins the sequence only where a page owes straight answers (live on loop/). | Every section heading is a real h2 under the hero's single h1; nav, form, button and CTA obligations are their own rows. Geo variants are deferred to un-stealth — recorded in ../GUIDE.golive.md. | Per the page budget — flourish lives in the hero and the proof band, never on interactive elements. GUIDE.motion-register.md |
Deeper contracts — owned elsewhere, pointed at from here#
These are already behaviour law in their own documents. This table points; it does not fork.
| Contract | Owner |
|---|---|
Slide-up sheet mechanic (translateY-only, never a route change), tools panel, bell, menu, capture bar, segmented control | ../briefs/SPEC.app-components.md |
| Approve/Deny — primary single-tap, Deny gets a 5s undo, never symmetric weight | ../briefs/SPEC.app-components.md |
| The five relay-app primitives (Sheet, ControlRow/FilterSheet, ObjectCard, StateBlock, Rail), build order and APIs | ../briefs/BRIEF.ui-component-kit.md |
| Card head, Code presence, badge rule (DEC-025 L1–L4) | GUIDE.list-surfaces.md · ../briefs/BRIEF.code-presence.md |
| Empty / error / loading state copy | GUIDE.states-and-language.md |
Analytics hooks — data-rly-* in the component's own markup, never bound to a styling class | ../briefs/BRIEF.ui-component-kit.md §5b |
Tokens — how a consuming team gets the values#
Every value in the table's specimens resolves to a token. The contract for any team consuming these components:
- Source:
brand/relay-design-tokens.jsonis the only token file a human edits.scripts/build_tokens.pyemits the adapters intobrand/generated/— the--rly-*stylesheet this estate links, relay-app's bare--*set, React Native constants, MkDocs, and literal-hex constants for email. The build refuses to emit a palette that fails its own contrast invariants (label-on-fill ≥ 4.5:1 and fill-on-page ≥ 3:1, both themes). - App teams map the same token names into their platform — consume the emitted adapter for your platform; never fork the values, never hand-copy a hex. When a value looks wrong, the fix goes into the JSON source and re-emits everywhere.
- The consumers are owned by another session. This repo emits the adapters and never edits relay-app or relay-platform (EC, 2026-08-26: "you do NOT touch anything outside of creative for that token change"). If an adapter is missing something a consumer needs, the ask comes back here.
- Precedence: tokens win on any value conflict; this document wins on behaviour.
Amending#
Same house rule as every guideline: a row earns its place by catching a real defect. Change the table in a PR with the EXP-nnn finding or the dated quote that prompted it, add a line to CHANGELOG.md in the same PR, and update MANIFEST.md if it changes what's canonical. Reviewing against this table, name the row and the column — "the card fails its a11y cell: the link doesn't stretch" is actionable; "the card feels wrong" is not.
One open pick, on EC: the marketing button vocabulary (.btn-primary / .btn-ghost) and the app-concept button vocabulary (accent / ghost / danger) are two naming families for the same contract. The table records both; unifying the names is a decision, not a drift to absorb.