UI documents

The structure of a Polyxd UI document, how it binds to host data, how actions work, and why keys matter.

A UI document is one interface, generated or authored: a JSON object that lists semantic components, says which one is the root, and binds them to data the host provides. It is validated by packages/spec/schema/ui.schema.json, published at https://polyxd.com/schema/0.3/ui.schema.json.

A real example#

This is packages/spec/examples/money-send-confirm.json, the confirmation step of sending money:

{
  "$schema": "https://polyxd.com/schema/0.3/ui.schema.json",
  "specVersion": "0.3.0",
  "surface": {
    "id": "send-confirm",
    "title": "Confirm payment",
    "intent": "money.send",
    "pattern": "confirm-destructive"
  },
  "root": "confirm",
  "components": [
    {
      "id": "confirm",
      "component": "Confirm",
      "title": "Send £250.00 to Alex Kim?",
      "severity": "consequential",
      "consequence": "The money leaves your account immediately and can't be recalled.",
      "summary": "summary",
      "confirm": {
        "label": "Send £250.00",
        "action": {
          "event": {
            "name": "transfer.confirm",
            "context": { "quoteId": { "path": "/quote/id" } }
          }
        }
      }
    },
    {
      "id": "summary",
      "component": "DetailList",
      "items": [
        { "key": "recipient", "label": "To", "value": { "path": "/quote/recipient" } },
        { "key": "amount", "label": "Amount", "value": { "path": "/quote/amount" },
          "format": { "type": "currency", "currency": "GBP" } },
        { "key": "fee", "label": "Fee", "value": { "path": "/quote/fee" },
          "format": { "type": "currency", "currency": "GBP" } },
        { "key": "reference", "label": "Reference", "value": { "path": "/quote/reference" } }
      ]
    }
  ],
  "data": {
    "quote": { "id": "q_91", "recipient": "Alex Kim", "amount": 250, "fee": 0, "reference": "Rent share" }
  }
}

Structure#

Field Required What it is
$schema No The schema the document follows, https://polyxd.com/schema/0.3/ui.schema.json, so editors validate and complete it as you type.
specVersion Yes The spec version the document follows. Currently 0.3.x; 0.2.x and 0.1.x documents still validate.
surface Yes What this interface is for (see below).
root Yes Id of the top-level component.
components Yes A flat list of components.
data No A snapshot of host data, for tests and examples. In an app, the host passes data to the renderer instead.

The surface#

Field Required What it is
id Yes Id of the surface.
title Yes Short title (page title or dialog title). The web renderer shows it as the page's h1, except when the root is a Confirm.
intent No What the user is trying to do, as a stable key such as money.send. Interface memory and analytics are organised by intent.
pattern No Id of the pattern the surface follows. The validator runs that pattern's checks.
journey No Id of the journey this surface is a step of.
dismissible No Whether the surface can be dismissed. Defaults to true.
kind No surface (the default: a screen, panel or dialog that lives inside a shell) or shell (the product's frame: a Frame at the root with an Outlet for screens). A shell is always authored, and the shell components are allowed only in one; see Components.
origin No generated or authored: whether a generator wrote it for a request, or a person authored it as a screen of the product. Validation and rendering are the same either way; see Generated or authored.

A flat list of components#

Components are not nested. Each one has an id, and components refer to their children by id. This is the same adjacency-list shape A2UI uses, which is what makes A2UI export a near one-to-one projection and makes progressive streaming possible.

Every component has these common fields:

Field What it is
id Unique within the document. Starts with a letter; letters, digits, _ and -.
component One of the 44 component names.
key Optional stable semantic key (see Keys).
visible Optional boolean or binding. The component is hidden when it is false.
accessibility Optional {label, description, live, hidden}, the same fields as A2UI v1.0. Only needed to add to what the component's semantics already provide.

The validator checks the tree beyond the schema:

The validator is validateDocument in @polyxd/spec. Import it from @polyxd/spec/browser to use it anywhere: Node, browsers and Cloudflare Workers. Its schema check is compiled ahead of time. It never uses eval or new Function, so it also works on a page with a strict content security policy.

Bindings#

Any value that comes from the host is a binding: { "path": "<JSON Pointer>" }. Paths are RFC 6901 JSON Pointers into host data.

{ "component": "Metric", "label": "Balance", "value": { "path": "/account/balance" },
  "format": { "type": "currency", "currency": "GBP" } }

Many text props accept either a literal string or a binding. Input components (TextInput, Choice, Toggle, DateInput, RangeInput) bind their value two ways: the renderer writes the user's input back to that path.

Values are never pre-formatted by the generator. A format (text, number, currency, percent, date, time, datetime, relativeTime, duration) tells the renderer how to present a raw value, and the renderer localises it.

Relative paths inside repeated items#

A Collection repeats one component per item in an array. Inside that template, paths without a leading / resolve against the current item. From personal-reading-log.json:

{
  "id": "books",
  "component": "Collection",
  "label": "Books to read",
  "items": { "path": "/books", "componentId": "book" },
  "empty": "empty"
},
{
  "id": "book",
  "component": "Card",
  "title": { "path": "title" },
  "subtitle": { "path": "author" },
  "action": { "event": { "name": "book.open", "context": { "id": { "path": "id" } } } }
}

Some props are also item-scoped by definition: Table columns and rowAction resolve against each row, Chart x and series against each data point, and Comparison attributes and choose against each option. A relative path anywhere else is a validation error. An absolute path that doesn't exist in data (when data is given) is a warning.

Actions are capability intents#

An action never contains code. It names a capability the host has registered, plus the values to send with it:

{ "event": { "name": "task.save", "context": { "title": { "path": "/draft/title" } } } }

The renderer resolves the context bindings and calls the host's onAction({ name, context, source }). The host decides what happens. Capability names are dotted, lowercase-first identifiers such as transfer.confirm.

The ui. namespace is reserved for actions the renderer handles itself. Only three exist:

Action What the renderer does
ui.dismiss Closes the surface (calls onDismiss). The default cancel of a Confirm.
ui.back Goes to the previous step inside Steps.
ui.next Goes to the next step inside Steps.

Any other ui.* name is a validation error. With a capability registry, the verifier also checks that every action names a registered capability and that risky ones sit behind a confirmation. See Product.

Data comes from the host#

The generator lays out and labels data; it never supplies it. The schema enforces this where it matters most: Metric.value, Media.src, Table.rows and Chart.data must be bindings, and a Collection can only repeat over an array path in host data, so a document can't hard-code a balance or embed an image URL. The data field in a document is only a snapshot for tests and examples.

UI is data, never code#

A UI document has no scripts, no styles, no class names and no URLs. Media is a reference in host data that the host turns into a URL through resolveMedia. Everything visual comes from the design-system pack, and everything that happens comes from the host's handlers. That is what makes a generated surface safe to embed: the worst a bad document can do is fail validation or be ugly.

Keys and memory#

A key is a stable semantic name for a thing on screen, such as fee or recipient.name (lowercase, dotted, _ allowed). Ids are local to one document. Keys are meant to be the same across generations: if the fee appeared last time with the key fee, it should have the key fee next time too.

Keys can go on components and on items inside them (detail rows, table columns, steps, views, chart series, comparison attributes).

They matter because recognition depends on them. The verifier's consistency check matches two generations of the same intent by key, then compares which component each key uses, their relative order, and their labels. The runtime's interface memory keeps the screen shown for each intent on the client and gives it back to the model next time, with the instruction to keep its keys, structure, order and labels, so the same task keeps the same shape. See Verifier.

Polyxd is an early preview. Found something unclear? It will get better with your feedback.