Kyle Cooney
Tutorial · Part II · 2026

Bind the generated interface

Vercel Labs' json-render lets a model generate real interfaces: you define a catalog of components, the model emits a JSON spec constrained to it, and a renderer draws the result. That is the structural half of a bound generator, shipped as a library. This tutorial adds the other half, the adaptive design system: a behavior the screen serves, a brand the generator reads, a renderer contract you can run as code, a fallback that ships when generation fails, and evals that check twenty screens before a customer sees one.

This is Part II. Part I, the step-by-step tutorial, writes the governance files this one binds: commitments, a brand file, named behaviors. Run it first, or at least skim its artifacts. Plan on one weekend. The running example is Corredor's transfer confirmation, the screen where a wrong generation costs real money.


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

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.

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:

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

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 · 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/
   reruns the 20-variant eval suite.
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. 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.