Skip to content
zabloo

The envelope

The unit an SDK loads: one JSON object carrying a version, a token dictionary, one or more views and an asset manifest — plus the props every node in it carries.

The envelope is the one file a game downloads to get its UI. Picture the guild shop screen: the panel, the two rows of items, the Buy buttons, the gold counter. All of it travels in a single JSON file — the boxes and labels, the colours they reference, the images they paint — and the SDK inside the game reads that file and draws the screen. There is no second file and no build step on the player’s machine.

Formally: one JSON object carrying a version, a token dictionary, one or more views, and — when the UI uses images — an asset manifest. The shape of what it carries is the IR, and these pages are its reference.

There is exactly one loading path. A file imported by hand in the editor and a hot-update pushed from the platform are the same versioned payload, read the same way.

{
  "v": 1,
  "tokens": {
    "color.primary": "#4f46e5",
    "space.3": 12,
    "radius.md": 6
  },
  "views": {
    "hud": { "type": "Container", "children": [] }
  },
  "assets": {
    "icons/coin.png": {
      "hash": "9f2c…",
      "mime": "image/png",
      "size": 1204,
      "width": 32,
      "height": 32,
      "data": "iVBORw0KGgo…"
    }
  }
}

Four top-level fields, and the rest of this reference describes what lives inside one of them:

PropTypeDefaultDescription
vnumberThe IR major version. An SDK refuses a payload whose major it does not implement.
tokensRecord<string, string | number>Flat design-token dictionary.
viewsRecord<string, ZNode>Documents (views, scenes) keyed by view id. At least one usable view is required.
assetsRecord<string, AssetEntry>absentAsset manifest keyed by logical id. Envelopes with no assets omit it.

Views

A view is one screen. An envelope carries several of them — a HUD, a shop, a settings screen — and the game chooses which one to render. Each value is a tree of nodes, the boxes and labels the screen is made of; the key is the id the game asks for.

View ids are opaque strings and may contain dots, which is why diagnostics address them in brackets: views["shop.main"].children[2].

Node ids are expected to be unique within a view. They are what the game names when it drives a node through the host channel, and what an anchored Overlay points at. Give a node the id audio-section and setOpen("audio-section", true) opens it from game code; the whole surface is on The host channel. Duplicates load, with a warning, and resolve to the first match.

Tokens

A token is a named value — color.primary, space.3 — that styles point at instead of spelling out. Styles do not bake values: they reference tokens, and the SDK resolves them per node at render time. Swapping the dictionary re-themes the whole UI without re-emitting the tree — which is what makes a theme hot-updatable on its own.

A token reference is a string wrapped in braces: "{color.primary}". It is valid anywhere a Dim or a ColorValue is accepted, and the dictionary is flat — the key is the whole name including its dots, so a lookup is one hash hit and never a walk:

"tokens": { "color.primary": "#4f46e5", "space.3": 12 }
"style":  { "background": "{color.primary}" },
"layout": { "padding": "{space.3}" }

In the shop, the Buy button asks for "background": "{color.primary}", and so does every other filled button in the game. Ship an envelope whose dictionary maps color.primary to a different hex and all of them repaint at once — the tree of nodes is byte-for-byte what it was.

There is no cascade and no inheritance. Every node carries its own resolved style; nothing is looked up from a parent.

A missing token never breaks the frame

The load pass reports it once, naming the node and the property (unknown-token), and then:

  • A Dim falls back to the property’s own default. A missing {space.3} gives padding: 0, and a missing {size.card} on a width leaves the node auto-sized.
  • A declared ColorValue paints the missing-color magenta. It is a deliberate, loud signal: the author asked for a color and named one that does not exist, and a silently transparent node would hide the typo instead of showing it. An absent color is not this case — it simply paints nothing.

Token values are strings or numbers, and the property decides how to read one: a Dim takes the number, a ColorValue takes the string. A token of the wrong type is treated exactly like a missing one.

Assets

Images travel inside the envelope. An entry describes the content and, in v1, carries the bytes:

PropTypeDefaultDescription
hashstringContent identity (SHA-256, hex). Deduplicates today; the key for content-addressed caching later.
mimestringe.g. "image/png". The format is generic — which MIME types are accepted is an export concern.
sizenumberByte size of the decoded content.
widthnumberabsentPixel width. Lets layout reserve space before the bytes are decoded.
heightnumberabsentPixel height.
datastringabsentThe content, base64-encoded.

Nodes reference an entry by its manifest key, prefixed: "asset:icons/coin.png". The prefix is what makes an asset reference recognizable without knowing the manifest — isAssetRef and assetIdFromRef in @zabloo/format are the shared readers for it.

data is optional in the schema only: a v1 export always inlines it. The field exists so a future delivery path can ship an envelope that names its assets by hash and lets the SDK resolve the bytes from a cache or a CDN, without a format change. An SDK that finds no data and cannot resolve the bytes paints the node’s background and nothing else — the same as an image still being decoded.

width and height matter for layout, not for paint: an Image takes the source’s pixel size as its intrinsic size, so a manifest that omits them makes the node measure as zero until it is given an explicit size.

Fonts are not assets in v1

The manifest carries images only — the export accepts .png, .jpg and .jpeg — and no node has a prop that could name a font, so there is nothing to reference one with. Every target rasterizes the same embedded typeface instead. Per-project fonts arrive with the text engine work, and they will land here: the manifest is deliberately generic about MIME types, so adding one is an export concern rather than a format change.

The node base

Every node in views is an object with a type and the fields below. These are the fields every node has, whatever it is: a Button, a Text and a ScrollView all carry them. Each node type then adds its own — a Text adds text, a Slider adds min and max — and those are documented on each catalog page.

PropTypeDefaultDescription
typestringThe node's identity. Drives its behavior and its default paint.
idstringabsentAddressable name within the view.
visibleBindable<boolean>trueThe single hiding mechanism, with display:none semantics — a hidden node leaves the layout entirely.
disabledBindable<boolean>falseTakes this node and its subtree out of the interaction model.
layoutLayout{}Flex inputs — direction, gap, padding, size, grow, wrap.
styleStyle{}How the node paints, resolved per node.
statesPartial<Record<StateName, { style?: Style }>>{}Per-state style overrides, merged in one normative order.
transitionTransitionabsentTweens this node's animatable values when they change.
autofocusbooleanfalseThis node takes the initial focus of its scope.
clipbooleanfalseClips children's paint and hit-testing to this node's rect.

Three of them carry rules worth stating outright.

visible is the only way to hide something. There is no second mechanism — no display, no hidden, no opacity trick with layout consequences. Hiding a node removes it from the layout pass, so its siblings close the gap; showing it again brings it back. Because it is Bindable, the game opens and closes UI by moving a boolean in its own data.

disabled is the only prop that inherits. A node’s effective value is its own or any ancestor’s, so one prop switches a whole form section off; an Overlay restarts the chain, being the top of its own input scope. It removes focus, hover, press and actions — not paint, and not scrolling — and what it looks like is states.disabled, since the format ships no built-in dimmed look. The whole rule is on Input & focus.

clip is paint configuration, not runtime state. A node’s effective clipping rect is the intersection of its own with every ancestor’s, and it cuts input as well as pixels: a child painted outside the rect is not tappable there either. A ScrollView always clips and ignores an explicit clip: false.

What can be bound

A binding is an address into the game’s own data — player.gold — written where a fixed value would go, so the node shows whatever the game currently has there.

visible and disabled are the two base props that take a binding in place of a literal, and they are the same two on every node type: the game pushes a boolean and the node enters or leaves the layout, or the whole subtree drops out of the interaction model. Everything else that binds is type-specific — a Text’s text, a Toggle’s checked, a Repeat’s items — and lives on Bindings & actions.

Note that style is not bindable anywhere. A bar that changes color with its value is done by the game moving a token, not by the UI computing one.

Where the tree comes from

@zabloo/react emits envelopes. zabloo export renders the project’s views, collects the assets they reference, and writes the JSON. Authoring-time concepts — user components, variants, composites — are resolved during that pass and never appear in the output.

Reading an envelope back — parsing, validating, repairing — is Loading.