An A2UI (Agent-to-UI) renderer built on BindJS, with native rendering across web, Apple, and Android platforms using React, SwiftUI, and Jetpack Compose.
One BindJS FlightCard source and one set of A2UI messages, from examples/ios/custom-catalog and examples/android/custom-catalog.
A tour of the examples on both phones, from the catalog to the custom components:
a2ui-bindjs.mp4
BindJS is the open component language for
agent UI: write a component once in JavaScript, with its logic, and render it with React,
SwiftUI or Jetpack Compose. It uses an API modeled on
SwiftUI, with a runtime that turns component definitions into a JSON AST for each platform
to render as native views. The same defineComponent source can produce a
UISegmentedControl on iOS and a <select> on the web.
This fits well with A2UI. A2UI describes components and binds them to data, leaving their appearance to a catalog. With BindJS, that catalog can be shared across all three platforms.
- One shared catalog. The 18 components in
core/src/catalog/basicpower the web examples, the SwiftUI app, and the Compose app in this repo. Changes to those components apply across all three. - Native rendering. On Apple and Android platforms, components render through SwiftUI and Jetpack Compose, with native gestures, animations, and typography.
- A shared protocol engine. The TypeScript interpreter bundles into a single dependency-free file, about 34 KB gzipped. Native apps embed it to handle parsing, data, functions, and diffing without implementing the protocol themselves.
- Catalog updates at runtime. Hosts can fetch and register updated component
definitions without an app release.
examples/web/metabindloads a catalog from a Metabind project, whileexamples/web/custom-catalogshows how to override individual components.
| Version | Support | Notes |
|---|---|---|
| v1.0 | Full | Rendered and validated against the vendored schemas |
| v0.9.1 | Full | Same components and functions, so one catalog answers to both |
| Message | Direction | Support |
|---|---|---|
createSurface |
agent → renderer | Web, iOS, Android |
updateComponents |
agent → renderer | Web, iOS, Android |
updateDataModel |
agent → renderer | Web, iOS, Android |
deleteSurface |
agent → renderer | Web, iOS, Android |
callRendererFunction |
agent → renderer | Parsed and dispatched as a store event for hosts to subscribe to |
agentFunctionResponse |
agent → renderer | Parsed and dispatched as a store event for hosts to subscribe to |
action |
renderer → agent | Web, iOS, Android |
error |
renderer → agent | Web, iOS, Android (opt-in, per validate) |
| Components | Support |
|---|---|
AudioPlayer Button Card CheckBox ChoicePicker Column DateTimeInput Divider Icon Image List Modal Row Tabs Text TextField Video Slider |
Web, iOS, Android |
| Functions | Support |
|---|---|
and or not length required numeric email regex pluralize formatString formatNumber formatCurrency formatDate openUrl |
Web, iOS, Android |
| Suite | Result |
|---|---|
Official conformance suite (vendor/conformance/) |
20 passed, 0 known gaps |
| 43 official spec examples, v1.0 | Rendered and schema-validated |
| 43 official spec examples, v0.9 | Rendered |
| Both example sets again on iOS and Android | Decoded into native view trees |
Install the renderer and the core package its message types come from:
pnpm add @metabindai/a2ui-bindjs @metabindai/a2ui-bindjs-reactFor a step-by-step walk through a Vite + React app, chips and a component override included, see Getting started.
Add the package to your Package.swift, then depend on the A2UI product:
.package(url: "https://github.com/metabindai/a2ui-bindjs.git", from: "0.1.0")Add the artifact to your module's dependencies:
implementation("ai.metabind:a2ui-bindjs-android:0.1.0")Note
The artifact is served from GitHub Packages, which requires a token even to read. See Consuming the artifact for the repository configuration, the minimum SDK, and the compile settings the AAR expects.
The top level splits by platform. vendor/spec/ is the protocol itself.
| Path | Package | Role |
|---|---|---|
vendor/spec/ |
n/a | Verbatim A2UI v1.0 schemas and examples. Never edited; the build reads it. |
vendor/conformance/ |
n/a | The official A2UI conformance suite, vendored. pnpm test runs it. |
core/ |
@metabindai/a2ui-bindjs |
Protocol, store, function library, rendering engine, catalog. No React. |
react/ |
@metabindai/a2ui-bindjs-react |
<A2UIRenderer /> and hooks, on the BindJS web renderer. Depends on core. |
ios/ |
a2ui-bindjs-apple (Swift) |
A2UIHost and A2UISurfaceView. Embeds core's renderer bundle. |
android/ |
ai.metabind:a2ui-bindjs-android |
A2UIHost and A2UISurfaceView on Compose. Embeds the same bundle. |
examples/ |
private | Grouped by platform: web/, ios/, and android/. |
Core is published and React consumes it as a package, so a host that renders natively, or on a platform React never reaches, takes core alone.
| Path | What it shows |
|---|---|
examples/web/minimal |
The smallest thing that renders a surface, action round-trip included |
examples/web/custom-catalog |
Two components overridden with sources plus one catalog entry, no runtime needed |
examples/web/metabind |
Catalog components fetched from a Metabind project rather than bundled |
examples/web/mcp |
A2UI over MCP: tools that answer with a UI instead of prose |
examples/web/playground |
Monaco message-stream editor, scrubbable timeline, and the 43 official spec examples |
examples/ios/minimal |
The same protocol drawn in SwiftUI, sharing one BindJS runtime with the host |
examples/ios/catalog |
Every basic-catalog component on its own screen, each a real A2UI surface |
examples/ios/custom-catalog |
A component the app registers and the agent names, using the web example's source unchanged |
examples/android/minimal |
The same surface again, message for message, on Jetpack Compose |
examples/android/catalog |
The catalog screens on Compose, decoded through the sandbox |
examples/android/custom-catalog |
useCatalog on Android, sharing that same Rating source |
Build and test everything, then start whichever example you want:
pnpm install
pnpm build # every package and example
pnpm test
pnpm dev:minimal # :5182, start here
pnpm dev:custom-catalog # :5183
pnpm dev:metabind # :5184
pnpm dev:mcp # :5185 + MCP server on :8787
pnpm dev:playground # :5181 (also `pnpm dev`)
pnpm dev:ios # the minimal example on an iOS simulator
pnpm dev:ios:catalog # every catalog component, on a simulator
pnpm dev:ios:custom-catalog # a component the app registers and the agent names
pnpm dev:ios:mac # the minimal example as a macOS window
pnpm dev:android # the same three, starting an emulator if none is running
pnpm dev:android:catalog
pnpm dev:android:custom-catalogBuild a store from the agent's messages, then hand it to the renderer:
import type { ActionMessage, AgentMessage } from '@metabindai/a2ui-bindjs'
import { useA2UIStore, A2UIRenderer } from '@metabindai/a2ui-bindjs-react'
function Surface({ messages, sendToAgent }: { messages: AgentMessage[]; sendToAgent: (action: ActionMessage) => void }) {
const { store } = useA2UIStore(messages)
return <A2UIRenderer store={store} onAction={sendToAgent} />
}Create a host, subscribe to actions, apply messages, and put the view in a SwiftUI body:
import A2UI
let host = A2UIHost()
host.onAction = { action in
// send it to the agent
}
host.apply(messagesFromTheAgent)
A2UISurfaceView(host: host)The same shape on Compose. apply suspends, and A2UISurfaceView is a composable:
import ai.metabind.a2ui.A2UIHost
import ai.metabind.a2ui.A2UISurfaceView
val host = A2UIHost(context)
host.apply(messagesFromTheAgent)
host.actions.collect { action ->
// send it to the agent
}
A2UISurfaceView(host = host)