Concepts

A2UI in five ideas, then the SDK's five words: catalog, theme, extension, host plugin, vapp.

Generative UI

A chat answer is text. A generative UI answer is an interface: a card with the numbers, a form for the missing fields, a button that does the next step. The model decides what to show. The app decides how it looks and what it is allowed to do.

A2UI is the open protocol for this. The agent sends UI as data, never as code: components from a catalog both sides agree on, a data model, and the actions it wants back. A client renders that with its own components. Exponential UI is a set of A2UI clients: one catalog, a TypeScript reference and a Rust core, and native renderers for React, SwiftUI, Jetpack Compose and gpui. It speaks A2UI v0.9 (the pinned v0.9.1 spec, vendored byte for byte).

WordWhat it is
SurfaceOne rendered UI: a component tree, its data model and its theme.
CatalogThe components a surface may use and their props. The model reads it; the renderer implements it.
ThemeA JSON file of tokens and component recipes. Anyone can write one; renderers load it at runtime.
ExtensionExtra components: an extension catalog plus a painter per platform, registered with the renderer.
Host pluginWhat the embedding app lends a surface: transport, functions, data bindings, policy.
VappAn app built on all of the above: templates, data, bindings and a theme in one declarative package.

Surfaces and messages

An agent talks to a client in four messages: createSurface (an id and the catalog it will use), updateComponents (components, added or replaced by id), updateDataModel (a value at a JSON pointer) and deleteSurface. They stream one per line as JSONL, over SSE, a WebSocket or inside an MCP tool result.

jsonl
{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://ui.exponential.at/catalogs/core/v1"}}
{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[
  {"id":"root","component":"Card","title":"Deploy 0.18.84?","children":["note","go"]},
  {"id":"note","component":"Text","text":{"path":"/summary"}},
  {"id":"go","component":"Button","label":"Deploy","on":{"press":{"event":{"name":"deploy","context":{"version":"0.18.84"}}}}}
]}}
{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/summary","value":"3 platforms, all checks green."}}

Components arrive as a flat list that references children by id, so a model can stream them in any order and patch one without resending the tree. The renderer rebuilds the tree, maps A2UI basic components onto the core catalog, expands macros and paints. A component it does not know becomes a visible placeholder and a reported issue, never a crash.

Data model

Every surface has one JSON data model. A prop marked bindable can take {"path": "/summary"} instead of a literal, and it follows the model: after an updateDataModel, every node bound to that path shows the new value. Inputs bind both ways, so what the user types lands at the same path.

A list repeats a template per item. The children name a component and an array path, and relative paths inside the template read the current item:

jsonl
{"id":"people","component":"List","children":{"componentId":"person","path":"/people"}}
{"id":"person","component":"Row","title":{"path":"name"},"subtitle":{"path":"role"}}

Bindings resolve when the renderer binds the tree to the data, after macros have expanded. That is why the result is the same on every platform, macros included.

Also on every surface: a visible condition on every component, a key per template item (reordering keeps focus and input), per-surface locale, time zone, strings, mode, density and contrast, and host commands (focus, announce, scroll into view, scroll to index).

Actions and functions

Interactive components fire events, and on.<event> says what happens. An event action goes back to the agent with its context resolved against the data model:

json
{"version":"v0.9","action":{"name":"deploy","surfaceId":"main","sourceComponentId":"go",
  "timestamp":"2026-10-08T09:14:02Z","context":{"version":"0.18.84"}}}

A function call runs on the client. 31 functions are built in on every renderer: the 14 A2UI basic ones (checks, formatting, openUrl, and/or/not) and the core ones (value functions, set). A host can register its own behind a policy gate (see host plugins):

json
{"id":"docs","component":"Button","label":"Open the docs",
 "on":{"press":{"functionCall":{"call":"openUrl","args":{"url":"https://ui.exponential.at/"}}}}}

Buttons are optimistic: they stay pending until the host has handled the action. Text fields are host-owned: edits go out debounced with a revision, so a slow round trip never drops a keystroke.

The core catalog

The catalog is the components a surface may use. The core catalog, https://ui.exponential.at/catalogs/core/v1, has 70 components with shadcn's names, variants and sizes (Button, Card, Tabs, Select, Dialog, Chart …). Every component and prop has one sentence written for the model; catalogPrompt() turns them into a compact system prompt.

  • 44 natives each renderer paints itself, such as Box, Text, Input and Popover.
  • 26 macros expand into natives before painting, such as Card, Badge, Alert and Sidebar. A renderer never implements a macro, and every part it expands into stays themable.
  • Core lite (https://ui.exponential.at/catalogs/core-lite/v1) is the subset without overlays, media and charts, for small screens and smaller prompts.

A client advertises the catalog ids it supports (core, core lite, A2UI basic, then its extensions), and a surface names one in createSurface. A surface that asks for a catalog the client lacks is refused with an error the agent can read.

A2UI basic, mapped

A2UI ships a small basic catalog. Exponential UI vendors it unchanged and maps it onto the core catalog through one table, so a surface written for any A2UI client renders here too and painters only ever learn one vocabulary. Dynamic values pass through untouched, the basic icon names map onto the icon registry, and the 14 basic functions are the same.

A2UI basicCoreMapping
TextTexttextVariant
ImageImageimageVariant
IconIconiconName
VideoVideoprops renamed
AudioPlayerAudioPlayerprops renamed
RowStackprops renamed
ColumnStackprops renamed
ListListprops renamed
CardCardprops renamed
TabsTabstabs
ModalDialogmodal
DividerSeparatorprops renamed
ButtonButtonbuttonChild
TextFieldInputtextField
CheckBoxCheckboxfieldName
ChoicePickerRadiochoicePicker
SliderSliderprops renamed
DateTimeInputDatePickerfieldName

Named transforms (textVariant turns h1…h4 into a Heading, for example) are listed in basic-map.json so a second implementation knows the rules.

Themes

A theme is data, never code: one JSON file of tokens (colours per mode, radius, spacing, type, control heights, shadows, motion) and recipes (per component part, a style for each combination of variant and state). Every renderer loads it at runtime, so a theme can come from a URL, a vapp or a server.

json
{
  "id": "brand", "name": "Brand", "extends": "neutral",
  "modes": { "dark": { "color": { "primary": "#818cf8" } } },
  "tokens": { "radius": { "md": 10 } },
  "recipes": {
    "Button": { "root": [
      { "style": { "borderRadius": "$radius.full" } },
      { "when": { "variant": "outline", "state": "hover" }, "style": { "backgroundColor": "$color.accent" } }
    ] }
  }
}

extends overrides only what changes. Three themes are built in: neutral (stock shadcn, the root), exponential (the zinc glass of the Exponential apps) and playful (deliberately different, the one conformance renders everywhere). See them side by side on Themes, build one in the theme builder, or follow Write your own theme.

Extensions

An extension adds components. It is an extension catalog (its own id, extending the core, never shadowing a core name) plus, for each native component, a painter per platform registered with the renderer. A macro component needs no painter at all: it expands into core components everywhere.

json
{
  "id": "https://example.com/catalogs/metrics/v1",
  "extends": "https://ui.exponential.at/catalogs/core/v1",
  "components": {
    "TrendLine": { "kind": "native", "props": { "values": { "type": "array", "required": true } }, … },
    "Metric":    { "kind": "macro",  "props": { "label": …, "value": …, "history": … }, … }
  },
  "macros": { "Metric": { "root": { "component": "Card", "children": [ …Text, Text, TrendLine ] } } }
}

The model learns the new components from the same prompt (catalogPrompt({extensions})), and the client advertises the extension's id. The Exponential app's issue rows, run rows and chips are the first extension, painted by the app's own components on all four clients. See Write an extension.

Host plugins

A surface needs a few things from the app that embeds it, the host. It is the same contract on all four platforms:

  • Transport: messages in, actions out. JSONL streams, SSE, WebSocket, MCP or in memory.
  • Functions: the names a surface may call beyond the built-ins (cart.add, app.toast).
  • Data bindings: bindDataModel points a path at a source URI such as weather:current?city=Vienna; the host's resolver for that scheme keeps it filled.
  • Policy hooks: which functions run (allow, ask, deny), which URLs open, which media requests carry credentials.

The function gate is strict: deny wins, then allow, then ask (your consent UI), then the default. The same rules are fixtures every renderer replays, so a policy means the same thing everywhere. See Write a host plugin.

Vapps

A vapp is an app built on all of the above and shipped as data: a declarative package of templates (components, initial data, bindings), the functions it may call and its theme. Any host runs it with createVappHost, and its functions list becomes the policy for its surfaces: listed names are allowed, everything else is denied.

json
{
  "id": "com.example.hello", "name": "Hello", "version": "1.0.0",
  "catalogId": "https://ui.exponential.at/catalogs/core/v1",
  "theme": "neutral",
  "functions": ["app.toast"],
  "templates": {
    "main": {
      "components": [ { "id": "root", "component": "Card", … } ],
      "data": { "title": "Hello, world" },
      "bindings": [ { "path": "/weather", "source": "weather:current?city=Vienna" } ]
    }
  }
}

Against an Exponential instance, a connector does the MCP OAuth grant and serves the instance's issues, boards, teams and members as exp: sources. Vapps run on the vapps platform; to build one, see Build and embed a vapp.

How the renderers agree

Two implementations hold the rules: the TypeScript reference in @exponential-at/ui and the Rust core exponential-ui, which replays the same fixtures byte for byte. The React renderer runs on the TypeScript side. SwiftUI and Compose reach the Rust core through a UniFFI facade, and gpui links it directly.

text
A2UI messages ─▶ reducer ─▶ UiNode tree ─▶ layout (taffy) ─▶ frames + resolved visuals ─▶ painter
               (basic map,     (bindings, parts,       (text measured in batches
                macros,         layers, lists)          by the platform)
                extensions)

The core lays out every native surface. The platform only measures text, in batches with at most three calls per pass, and paints each node at its frame with its own views. The web follows the same layout with CSS, held within a pixel of the core's frames. Painters never read a theme; they get resolved visuals per part and state. The conformance suite checks all of it, case by case, on every renderer.