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 generated interface: 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 ($id https://polyxd.com/schema/0.1/ui.schema.json).

A real example#

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

{
  "specVersion": "0.1.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
specVersion Yes The spec version the document follows. Currently 0.1.x.
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.

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 24 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:

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 model. 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 model 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. Planned interface memory will record the same things per user and feed them back to the generator as constraints, 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.