Skip to content
Kyle Cooney

A practical guide

Bind the generated interface

A hands-on tutorial pairing The Adaptive Brand with Vercel Labs' json-render. In ten steps you will define a catalog the generator can read, put a behavior under a renderer contract you can run as code, ship a deterministic fallback, eval twenty generated screens before one reaches a customer, and leave a harness that reruns every check in one command. One weekend, real code.

Let’s get into it
A practical guide by Kyle Cooney
One coral hub connects to chat, phone and dashboard frames that preserve the same coral disk and three fact markers.
Different interfaces. The same facts and intent.
A little space to think
In this piece 12 sections

Why this pairing works: json-render's spec is JSON, and JSON is checkable. The moment a generated screen becomes a walkable tree instead of a stream of markup, every placement guarantee in the article stops being an aspiration and becomes a function. Here is the map between the library's vocabulary and the system's:

json-render gives youIn system termsWhat this tutorial adds
catalog + zod schemaA design system the generator can read; structural bindingSemantic props only, no style knobs, facts by state path
registryRendering, wired to your real componentsComponents that consume semantic tokens and nothing else
generated specThe screen as an output, not a designA behavior that says what the screen is for
schema validationChecks the nouns are legalA renderer contract that checks the promises are kept
actionsWhat the interface may doConsent shape and authority on the risky ones
streaming patchesProgressive renderingA gate so consent never renders before the fee

One sentence to hold through all ten steps: the catalog can say a button exists; only a contract can say the fee is stated before the confirm button can be reached. The library checks structure. You will check promises.

Step 1 · 30 minutes

Stand up the renderer

Install the library and render one hardcoded spec, so every later step has a live surface to break:

npm install @json-render/core @json-render/react zod

Then the smallest loop, a catalog with one component, a registry that maps it to a real implementation, and a spec rendered by hand:

// catalog.ts
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react/schema";
import { z } from "zod";

export const catalog = defineCatalog(schema, {
  components: {
    Card: { props: z.object({ title: z.string() }), slots: ["default"] },
  },
});

// page.tsx: StateProvider + VisibilityProvider wrap
// a Renderer that takes { spec, registry }.

Hardcode a spec with a root Card and confirm it draws. No AI yet. The point of this step is that the spec, a plain JSON object with a root id and an elements map, is the artifact everything else in this tutorial reads, checks, and falls back to.

Check before you continueA hardcoded spec renders through the registry, and you can console.log the spec and see the whole screen as data.

Go deeperChapter 2, From Screens to Behaviors: the screen as an output. The spec is that idea, serialized.

Step 2 · 30 minutes

Make tokens the only styling path

The registry is where your real design system enters. Every component you register consumes semantic tokens, and the catalog exposes zero style props. No color, no spacing, no variant that names an appearance. If the generator cannot reach around the token layer, a thousand generated screens stay siblings:

Audit the registry components against /styles/tokens.css:
every color, space, radius, and type size comes from a
semantic token. Then audit catalog.ts: remove any prop
that names an appearance (color, size, variant:"blue").
A prop may name a meaning (tone:"warning") only if a
brand rule says when that meaning applies.

This is the same three-layer discipline as the article: primitives exist, semantic tokens mean, and the generator touches only the middle layer. In json-render terms, the model cannot emit a hex value, because no prop accepts one.

LawA design system the generators cannot read is a memo, not a system.
Check before you continueAsk your agent to generate a "make it pop, bright red title" spec. The schema has nowhere to put the red. That refusal is the token layer working.

Go deeperChapter 4, One Source of Truth: the layer discipline, and the canvas, prototype, and product as three renderings of one repository.

Step 3 · 45 minutes

Define the catalog as your nouns

One decision shapes the catalog: where on the generative spectrum this surface sits. At one end, hand-designed components that appear just in time, low latency, low variance, the same score card rendered the same way for everyone. At the other, fully model-driven interfaces, slower and freer, for surfaces whose users will trade seconds for range. The latency budget and the variance tolerance decide the position. The contract holds at every point on the spectrum, so the steps that follow do not change with your answer.

Now build the real catalog for the confirmation screen. Two rules make it governable. First, name components by function, never appearance: a FeeLine survives a redesign, a BlueFeeCard does not. Second, and this is the rule that pays for the whole tutorial: facts arrive by state path, never by generated prose. The model composes the screen; it does not get to write the numbers.

// catalog.ts, Corredor's confirm vocabulary
components: {
  Stack:           { props: z.object({ gap: z.enum(["flow","tight"]) }),
                     slots: ["default"] },
  TransferSummary: { props: z.object({ recipientPath: z.string(),
                                       amountPath: z.string() }) },
  FeeLine:         { props: z.object({ feePath: z.string() }) },
  RateNote:        { props: z.object({ ratePath: z.string(),
                                       etaPath: z.string() }) },
  WarnBanner:      { props: z.object({ messageKey: z.string() }) },
  ConsentButton:   { props: z.object({ labelKey: z.string(),
                                       amountPath: z.string() }) },
  CancelLink:      { props: z.object({}) },
},
actions: {
  confirm_transfer: { params: z.object({ transferId: z.string() }),
                      description: "Requires explicit consent" },
  cancel:           { params: z.object({}) },
}

The props are the point: every one is a path into state (feePath) or a key into your tokenized copy (messageKey), resolved by the renderer's $state binding. The fee the customer sees comes from the rate object, resolved at render time. A generator that cannot write an amount cannot invent one, which retires the whole category of Air Canada failures at the schema layer.

Check before you continueGrep the catalog for any z.string() prop that would accept free text a customer reads. Each one is either a copy key, a state path, or a bug.

Go deeperChapter 5, Semantic Components: nouns and verbs, and why a screen is where the two axes meet.

Step 4 · 30 minutes

Write the behavior the screen serves

You wrote this file in Part I, step 5; if you skipped Part I, the shape below is complete enough to write now. The catalog says what can be drawn. The behavior says what the screen is for, and it is the file the next two steps compile from. One upgrade makes it renderer-ready: the allowedActions must name the catalog's actions exactly, so the two files can be checked against each other.

// behaviors/confirm-transfer.ts
export const ConfirmTransfer = {
  intent: "Collect informed consent for a money transfer",
  requiredFacts: ["amount", "rate", "fee", "arrivalWindow"],
  allowedActions: ["confirm_transfer", "cancel"],
  forbiddenClaims: ["guaranteed arrival time"],
  repair: "RepairTransferDetails",
  evals: ["consent-is-explicit", "fee-before-consent"],
};

Keep it surface-free. Nothing in this file mentions json-render, React, or a screen, because the same behavior also renders as a WhatsApp message and a voice turn. The catalog is one renderer's vocabulary for it.

LawStop designing responses. Design behaviors.
Check before you continueEvery requiredFact maps to a state path a catalog component can bind, and every allowedAction exists in the catalog's actions. If a fact has no component, the vocabulary is missing a noun.

Go deeperChapter 5 for the vocabulary; Chapter 7 for the same behavior wearing other channels' clothes.

Step 5 · 45 minutes

Brief the generator

Now the generation call. The catalog constrains the output shape; the briefing decides what a good screen is. Compile the system context from the files that already exist, in this order:

const system = [
  read("skills/brand.md"),        // taste as constraints
  read("skills/commitments.md"),  // what must always be true
  describe(ConfirmTransfer),      // the behavior being rendered
  catalogDescriptions,            // what each component is for
].join("\n\n");

// Then your model call of choice, constrained to the
// catalog's spec schema, streaming JSONL patches.

The prompt itself stays small, one paragraph of situation: which customer, which corridor, which profile. The context is the accumulated system. This is the design-time control plane in miniature: the brief is compiled, not typed.

Check before you continueGenerate the confirm screen three times. All three are made only of catalog components, and no two are identical. Constrained but not frozen is the target.

Go deeperChapter 6, Brand as Governed Context: the brand file teaches the generator. The next step is the half that checks its work. OpenAI's Codex designers describe the same move as working at the skill level: define the design language there, reference the tokens, and build evals against it.

Step 6 · 1 hour

Write the renderer contract as code

Here is the step no library ships, because it encodes your promises, not a schema. The spec is a tree; walk it. Every placement guarantee from the article becomes an assertion over traversal order:

Speech-shaped branches grow inside a stone frame beside a locked gate and its matching key.
Expression has a boundary. Action needs authority.The renderer contract bounds what the generated interface may display and which actions it may expose.
// contracts/confirm-renderer.ts
// Walk spec.elements from spec.root, depth-first,
// collecting the visual order. Then assert:

fee-before-consent:   index(FeeLine) is less than
                      index(ConsentButton)
cancel-within-reach:  a CancelLink exists at depth
                      no deeper than ConsentButton
consent-is-explicit:  ConsentButton.labelKey names the
                      action and binds amountPath
facts-are-bound:      every *Path prop resolves against
                      the transfer's real state shape
no-orphan-warnings:   if state.holdReason exists, a
                      WarnBanner precedes ConsentButton

onFailure: render templates/confirm-plain.json

Fifty lines of TypeScript, no model in the loop, runs in microseconds. The difference in kind from step 1's schema check is the whole point: the schema guarantees every element is a legal noun; the contract guarantees the sentence they form keeps the promise. Run it on every generated spec before the renderer sees it, and on every streamed patch set once the tree is complete.

LawThe system that briefs the agent must bind the model.
Check before you continueAsk the agent to generate a spec that puts the fee below the confirm button. The contract rejects it and names the guarantee it broke. If it passes, your walk order is wrong.

Go deeperChapter 7, Every Surface, One Promise: renderer contracts as accessibility testing, placement and reachability, not presence.

Step 7 · 30 minutes

Ship the fallback spec

The fallback is the best argument for spec-based UI: it is not a second codebase, it is one more spec, written by hand, checked into git, that satisfies the contract with zero generation:

// templates/confirm-plain.json
{ "root": "stack-1",
  "elements": {
    "stack-1":  { "type": "Stack", "props": { "gap": "flow" },
                  "children": ["summary","fee","rate","consent","cancel"] },
    "summary":  { "type": "TransferSummary",
                  "props": { "recipientPath": "$state.transfer.recipient",
                             "amountPath": "$state.transfer.amount" } },
    "fee":      { "type": "FeeLine",
                  "props": { "feePath": "$state.transfer.fee" } },
    ...
  } }

Wire onValidationFailure to render this file. It will be plain. It will also be correct, and correct-but-plain is the right floor for a screen that moves money. One more wire while you are here: streaming. Progressive rendering means elements draw as patches arrive, so gate the ConsentButton's visibility on the fee being mounted. The contract guarantees order in the finished tree; the gate guarantees it during the stream.

LawThe channel changes the clothes, never the commitments.
Check before you continueBreak generation on purpose (return malformed JSON). The customer sees the plain template, complete and correct, and nothing logs to their screen. Then stream a valid spec slowly and confirm consent never renders before the fee.

Go deeperChapter 7 on fallbacks; Chapter 8 on degrade-to-template as the runtime plane's floor.

Step 8 · 45 minutes

Eval twenty screens before one ships

Everything is now in place for the only scale of checking that matters with generators: many variants, mechanically judged.

Interface panels pass a ruler, a magnifying glass and a plumb line; a rejected draft returns to the start.
Define the standard. Test before release.Exercise varied contexts before release; a good example is not evidence that the whole system is reliable.
Generate 20 specs for ConfirmTransfer across profiles
(first-time MX sender, repeat US sender, delayed transfer
with holdReason). For each spec, run:
1. the schema check (legal nouns)
2. the renderer contract (kept promises)
3. the behavior's evals from skills/evals.md
Report a table: variant, profile, pass/fail per check,
and the failing element id where it fails.

Then plant two violations: a spec with consent above the
fee, and a spec with a WarnBanner messageKey that does not
exist in the copy tokens. Confirm both are caught and
name which layer caught each.

The two planted violations should be caught by two different layers, the contract for the first, the schema-and-copy check for the second. That is the point of layering: each check is small, and together they leave no single place for a failure to hide. Every real failure this run surfaces is a decision: tighten the contract, sharpen the brand file, or add the missing noun. The fix always lands in a file.

LawWrite the taste down, then test against it.
Check before you continueThe table exists, both planted violations were caught, and you can say for each real failure which file gets the fix.

Go deeperChapter 10, Evaluation-Driven Design: floors and dials, and the rubric as a first-class deliverable.

Step 9 · 45 minutes

Build the eval harness

The twenty-screen run you just did was agent-driven: you asked, it generated, it judged, and the report scrolled away with the session. A harness turns that run into one command with a memory. Three artifacts, and the agent builds all of them:

Build the eval harness:

1. /evals/golden/: persist the step 8 scenarios as files,
   one JSON per scenario: the prompt, the profile, and the
   sample state it runs against. Ten to twenty is plenty.
2. /scripts/eval.ts, run as "pnpm eval": for every golden
   scenario, generate three specs (enough to see variance),
   then run the three layers (schema check, renderer
   contract, rubric evals from skills/evals.md). Write /evals/results/[date].json:
   pass or fail per variant per check, with the failing
   element id.
3. "pnpm eval --compare": diff the two most recent results
   files and report what newly fails and what newly passes.

The golden set is the taste made data. The runner makes checking free, which changes when checking happens: before a merge, after a catalog change, on a bored Tuesday. And the compare flag makes the sunset protocol executable instead of aspirational. When a new model ships, one command tells you which failures vanished, which scaffolding the model no longer needs, and which contract just earned its keep.

Keep the judge honest. The deterministic layers are code and stay code; only the prose rubrics need a model to judge them, and a different model than the generator makes what the article calls a usefully disagreeable second reviewer, because it shares none of the first one's habits.

LawMeasure behaviors, not generations.
Check before you continueRun pnpm eval twice: two dated results files exist and the compare reports zero drift. Then add a golden scenario that asks for consent above the fee and run again; the compare names the exact scenario and the check that failed it.

Go deeperChapter 10 for why the rubric is the craft; Chapter 13 for the sunset protocol this command serves. The same graduation, a golden set checked by hand growing into a harness, is how OpenAI's Codex designers describe scaling eval-driven design.

Step 10 · 30 minutes

Hook the loop

Last step: make forgetting impossible. Three hooks, then two lines in the log:

Add pre-merge hooks:
1. Any change to catalog.ts, behaviors/, or contracts/
   runs pnpm eval; new failures block the merge.
2. A new catalog component without a named behavior that
   uses it fails the merge.
3. A new action without a consent shape in its behavior
   fails the merge.

Then append to skills/decisions.md: why facts travel by
state path and not prose, and why the fallback is a spec
and not a second component tree.

And a sunset note, in the article's own spirit: the contract-walking code compensates for a generator that cannot yet be trusted with placement. If a future model holds the fee-before-consent instruction across a million generations, thin the contract to a sampled check and keep the commitment. The library will churn, the model will churn, and neither should take anything with it: your behaviors, contracts, evals, and fallback specs are files in your repository that would survive json-render's disappearance whole.

LawEvery layer is replaceable except the one that carries the promises.
Check before you continueCommit a catalog change on a branch without running anything. The hook runs the suite for you, and the merge waits for the result.

Go deeperChapter 13, The Case Against: the sunset protocol, and the tool-disappears test this step just passed.


When it goes sideways

  • The model keeps inventing component types. Your catalog descriptions are too thin. Each component's description should say when to use it, not what it is: "FeeLine: always present on any screen that can reach a ConsentButton."
  • The contract rejects everything. You are asserting layout, not promises. A contract that fails a spec because the summary is below the rate note is a style opinion wearing a badge. Demote it to an eval, keep the contract to commitments.
  • Streaming shows half-screens that feel broken. Gate at the behavior level, not per element: hold interactive elements until requiredFacts are all mounted, let everything informational stream freely.
  • The fallback drifted from the design. It is a spec; regenerate it. Ask the agent to propose an updated confirm-plain.json from the current catalog, run the contract on it, and diff before committing. A fallback nobody regenerates is the canvas problem all over again.

Where this tutorial ends and the article begins

This tutorial is one behavior, on one channel, under one contract, with the structural half supplied by a library. The full article is the system it belongs to. The map:

Tutorial stepArticle chapterWhat the article adds
1. RendererCh 2, From Screens to BehaviorsWhy the screen became an output
2. TokensCh 4, One Source of TruthThe three renderings; the prototype as eval surface
3. CatalogCh 5, Semantic ComponentsNouns, verbs, and where the axes meet
4. BehaviorCh 5, Ch 7The same behavior on chat, voice, and email
5. BriefingCh 6, Brand as Governed ContextThe full brand file and the never-say list
6. ContractCh 7, Every Surface, One PromisePlacement and reachability across every channel
7. FallbackCh 8, The Control Plane, TwiceThe full runtime plane the fallback belongs to
8. EvalsCh 10, Evaluation-Driven DesignFloors and dials; the experiment lane
9. HarnessCh 10, Ch 13Evidence that accumulates; the sunset made executable
10. HooksCh 11, Ch 13The truth loop and the sunset protocol

Read the full article for the argument, and Part I for the files this one assumed.

Find a thought.

A word, a question, or an idea you want to come back to.

Search stays on this page.ESC to close

Your margin notes.

A place for the things that stick. Saved on this device.

Select a passage as you read to save it here, or start with a thought of your own.

0 notes

Thanks for spending a little time here.

Continue the conversation Version history

A little more to think about.

All writing