Reference: The Vocabulary
The vocabulary is the closed set of everything a ChainRender app can express. It's machine-readable via
vocabulary(), and it's closed by design — that closure is what makes apps analyzable and safe. Every list in it is
derived from the real constants (the on-chain format dictionaries, the capability bits, the resolver-kind mirrors), so
tooling never hardcodes them.
import { vocabulary } from "@chainrender/client";
const v = vocabulary();
// v.nodeTypes the 9 node types and their own fields
// v.a11y accessibility fields (every node type except image and ref)
// v.behavior cond / step / anim / live (every node type except ref)
// v.enums allowed values: role, ariaState, variant, direction, inputType, provenance, anim, animRepeat
// v.capabilities the capability bits
// v.kinds IR sub-vocabularies: chart, control, layout
// v.resolverKinds every first-party resolver kind, with its args (most of the payload)
// v.limits maxNodes, maxDepth
Node types
The compiler reads exactly nine node types: text, box, button, input, icon, link, slot, image and
ref. Everything else you build is made of these:
- Headings, notes, captions are
textwith arole(compose.heading(...)is atextwithrole: "heading"). - Charts, tables, controls (toggle, select, slider, search), layouts (stack/grid/flow) and multi-view apps are
structured renderers with their own sub-vocabularies in
vocabulary().kinds, built with the format builders (buildTabs,buildWizard, …) and rendered by their own on-chain views. - Live data is a
slot(text) or animage(media) bound to a resolver kind.
Every field of every node type — which ones are required, the allowed enum values, which a11y and behaviour fields apply — is listed on Node Fields, generated from the same table the compiler is tested against.
Checking a tree before you compile it
compile() is lenient: it silently drops anything it doesn't read. A misspelled enum value (direction: "vertical"), a role that isn't in the dictionary (role: "note"), an unknown field, a11y fields on an image, or
args on a batch member all compile without an error — and ship without what you wrote. Check first:
import { checkNode, nodeJsonSchema } from "@chainrender/client"; // 0.10.0+
const r = checkNode(tree); // { ok, issues: [{ path, code, message }], nodeCount, depth }
if (!r.ok) console.table(r.issues); // e.g. /children/2/role enum role must be one of button, textbox, …
const schema = nodeJsonSchema(); // the same rules as a strict JSON Schema (draft 2020-12) for an AI tool
checkNode also enforces what JSON Schema can't express: at most 1024 nodes, depth at most 64, 255 children or
attributes per node, and 65,535 bytes per literal. A tree made in the visual builder carries builder-only fields —
argValues on slots and images (encoded into arg at export), image.scene, a root pinnedDep, and _-prefixed
design-time keys such as _multiview; check it with checkNode(tree, { authoring: true }) (SDK 0.10.1+).
Capabilities
READ_CHAIN (read state), WRITE_CHAIN (send a tx), VALUE (move ETH), SIGN_MESSAGE (typed signing),
BATCH_EXECUTE (atomic multi-action), CLIENT_FETCH (off-chain fetch — drops the grade), GPU_EXECUTE
(client GPU enhancement, fails closed to a poster). An app declares its envelope; the enforcer verifies it.
Resolver kinds
Each resolver declares which kinds it serves. vocabulary().resolverKinds lists every first-party kind with its
args, data edge and capability — never hardcode a kind string or guess an arg shape; read it from here. The full list
is on All Resolver Kinds. Encode a slot's arg exactly as the kind says: abi.encode of
its args unless the kind is marked argsEncoding: "packed" (raw bytes, e.g. ensName takes the 20 address bytes).
The kind list is most of the vocabulary's size, so an AI building one component rarely needs all of it. Through the
MCP server, request the vocabulary without resolverKinds and look kinds up with chainrender_search_kinds /
chainrender_describe_kind — see MCP Server & AI Skill.