___ ____ __ ______ __ __ _ __
/ | /__ / / / / / _/ / //_/(_) /_
/ /| | / __/ / / / // / / ,< / / __/
/ ___ |/ /__ / /_/ // / / /| |/ / /_
/_/ |_|\___/ \____/___/ /_/ |_/_/\__/
Headless A2UI for React - Zod-first, tree-shakeable
A React renderer for Google's A2UI (Agent-to-User Interface) protocol - enabling AI agents to generate rich, interactive UIs through declarative JSON.
Built with React, TypeScript, Next.js, Tailwind CSS, and Shadcn/UI.
A2UI is an open protocol by Google that allows AI agents to generate user interfaces through declarative JSON rather than executable code. Key features:
- Declarative JSON - Agents describe UI intent, not executable code
- Safe by design - Agents can only use pre-approved components from a catalog
- Streaming-first - JSONL format enables progressive rendering
- Framework-agnostic - The spec works with React, Lit, Angular, Flutter, etc.
This repo provides a React implementation of the A2UI v0.9 spec with a full demo app.
git clone https://github.com/maddhruv/a2ui-react.git
cd a2ui-react
npm install
npm run devOpen http://localhost:3000 to see the landing page. Try:
/playground- Interactive JSON editor with live A2UI rendering/examples- Form, cards, and streaming examples with mock agents/docs- Concepts, component catalog, and action system docs
Agent (Server) Renderer (Client)
+-----------------+ +------------------------+
| Generate A2UI | JSONL | Parse JSONL stream |
| JSON messages | -----------> | Manage surface state |
| (any LLM agent) | stream | Resolve component tree |
+-----------------+ | Render with Shadcn/UI |
^ | Handle data binding |
| Action events | Dispatch actions |
+------------------------ +------------------------+
Messages stream as JSONL (one JSON object per line):
{"version":"v0.9","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/specification/v0_9/basic_catalog.json"}}
{"version":"v0.9","updateDataModel":{"surfaceId":"main","path":"/form","value":{"name":""}}}
{"version":"v0.9","updateComponents":{"surfaceId":"main","components":[
{"id":"root","component":"Column","children":["title","input"]},
{"id":"title","component":"Text","text":"Hello","variant":"h2"},
{"id":"input","component":"TextField","label":"Name","value":{"path":"/form/name"}}
]}}A2UI uses a flat list with ID references instead of nested trees. This is optimized for LLM generation - models can emit components incrementally without tracking nesting depth:
[
{"id": "root", "component": "Column", "children": ["title", "btn"]},
{"id": "title", "component": "Text", "text": "Hello"},
{"id": "btn", "component": "Button", "child": "btn-text"},
{"id": "btn-text", "component": "Text", "text": "Click me"}
]import { A2UIProvider, A2UISurface, useA2UI } from "@/lib/a2ui";
function App() {
return (
<A2UIProvider onAction={async (event) => {
// Send action to your agent, return Response for more A2UI messages
return fetch("/api/agent", { method: "POST", body: JSON.stringify(event) });
}}>
<AgentUI />
</A2UIProvider>
);
}
function AgentUI() {
const { surfaceIds, connect } = useA2UI();
useEffect(() => {
connect("/api/agent/form");
}, [connect]);
return (
<>
{surfaceIds.map((id) => (
<A2UISurface key={id} surfaceId={id} />
))}
</>
);
}All 18 Basic Catalog components are implemented with Shadcn/UI:
| Component | Description | Shadcn Mapping |
|---|---|---|
| Text | Display text with variants (h1-h5, body, caption) | Native HTML elements |
| Button | Clickable with action dispatch | Shadcn Button |
| TextField | Text input with two-way data binding | Shadcn Input / Textarea |
| CheckBox | Toggle with two-way binding | Shadcn Checkbox |
| ChoicePicker | Dropdown select | Shadcn Select |
| Slider | Range input | Shadcn Slider |
| DateTimeInput | Date/time picker | Native date/time input |
| Image | Display image | Native img |
| Icon | Lucide icon by name | Lucide React |
| Row | Horizontal flex | div with flex-row |
| Column | Vertical flex | div with flex-col |
| List | List with optional templates | div with flex |
| Card | Container with title/subtitle | Shadcn Card |
| Tabs | Tabbed interface | Shadcn Tabs |
| Modal | Dialog with trigger | Shadcn Dialog |
| Divider | Separator line | Shadcn Separator |
| Video | Video player | Native video |
| AudioPlayer | Audio player | Native audio |
Components bind to a per-surface data model via JSON Pointer paths (RFC 6901):
{"id": "greeting", "component": "Text", "text": {"path": "/user/name"}}Input components support two-way binding - user changes update the data model immediately.
Button actions fire events with context resolved from the data model:
{
"action": {
"event": {
"name": "submit",
"context": {
"name": {"path": "/form/name"},
"email": {"path": "/form/email"}
}
}
}
}Override or extend the component registry:
<A2UIProvider registryOverrides={{ Text: MyCustomText }}>
...
</A2UIProvider>src/
lib/a2ui/ # Renderer library
types/ # TypeScript types (messages, components, data model, actions)
core/ # Core logic (JSON pointer, data store, surface manager, registry)
hooks/ # React hooks (useA2UI, useSurface, useDataBinding, useAction)
context/ # React context provider
components/ # Shadcn component implementations (18 components)
stream/ # JSONL stream parser
app/ # Next.js demo app
api/agent/ # Mock agent API routes
playground/ # Interactive JSON editor + renderer
examples/ # Form, cards, streaming examples
docs/ # Documentation pages
npm test # Run tests
npm run test:watch # Watch modeTests cover:
- JSON Pointer (RFC 6901) get/set/remove operations
- DataStore reactive state management
- SurfaceManager component lifecycle and root computation
- MessageProcessor message routing
MIT