A2UI Protocol
A2UI (Agent-to-User Interface) is a protocol that lets AI agents describe user interfaces using declarative JSON. The agent never touches the DOM — it generates a component tree spec, your application renders it using pre-approved components from your own catalog.
npm install @ainative/ai-kit-a2ui-core
npm install @ainative/ai-kit-a2ui # React renderer
A2UI is currently in alpha. The core library has 429 tests at 96% coverage. The React renderer implements 11 of 17 standard components. The remaining 6 are planned.
How it works
Agent Your App
───── ────────
Generates JSON spec → A2UIRenderer receives spec
{ type, props, Maps each node to a real
children: [...] } → React component via registry
Renders interactive UI
User interacts ← User clicks button, types, etc.
(WebSocket action) ← onAction callback fires
- Your agent responds with a JSON object following the A2UI component schema.
- Pass that JSON to
<A2UIRenderer>. It validates the spec and maps eachtypeto a component from the registry. - The default registry uses shadcn/ui components with full accessibility out of the box.
- User interactions (button clicks, text input, slider changes) are captured and sent back to the agent via the
onActioncallback or WebSocket transport. - The agent updates the spec in response to actions, and the renderer re-renders.
Security model
Agents generate JSON — not executable code. There is no eval(), no innerHTML injection, and no XSS surface. The component registry acts as the security boundary: only types listed in the registry can be rendered. Unknown types are silently ignored or surfaced as a debug placeholder in development.
Protocol format
Every A2UI spec is a JSON object with this shape:
interface A2UIComponent {
type: string; // Component type from the registry
props?: Record<string, unknown>; // Component properties
children?: A2UIComponent[] | string; // Child nodes or text content
}
A complete spec is a single root A2UIComponent. The renderer walks the tree recursively.
State bindings
Props can reference mutable state using JSON Pointer (RFC 6901). The renderer maintains a state object and resolves pointers at render time:
{
"type": "TextField",
"props": {
"label": "Your name",
"value": { "$ref": "/state/userName" },
"onChange": { "$action": "set:/state/userName" }
}
}
When the user types in the field, the renderer writes the new value back to /state/userName and re-renders the tree. This makes all components reactive without any custom code.
Event handlers
Button actions and other events are expressed as strings in the format verb:target:
| Pattern | Example | Meaning |
|---|---|---|
navigate:/path | navigate:/dashboard | Navigate to a route |
set:/state/key | set:/state/isOpen | Write a value to state |
emit:eventName | emit:formSubmit | Fire a named event to the onAction handler |
ws:methodName | ws:runAgent | Send a WebSocket message to the agent |
{
"type": "Button",
"props": {
"label": "Submit",
"variant": "primary",
"action": "emit:formSubmit"
}
}
Component reference
Implemented components (11)
Container
Layout wrapper. Controls direction, gap, and padding for its children.
{
"type": "Container",
"props": {
"direction": "column",
"gap": 16,
"padding": 24
},
"children": []
}
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'row' | 'column' | 'column' | Flex direction |
gap | number | 0 | Gap between children in pixels |
padding | number | 0 | Internal padding in pixels |
Heading
Section heading. Renders the appropriate <h1>–<h6> element.
{
"type": "Heading",
"props": { "level": 2 },
"children": "Edit Profile"
}
| Prop | Type | Default | Description |
|---|---|---|---|
level | 1 | 2 | 3 | 4 | 5 | 6 | 2 | Heading level |
children | string | required | Heading text |
Text
Paragraph text with optional visual variants.
{
"type": "Text",
"props": { "variant": "muted" },
"children": "Some descriptive copy here."
}
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'muted' | 'default' | muted renders in a subdued color for secondary text |
children | string | required | Text content |
Button
Clickable action button. Fires the action string on click.
{
"type": "Button",
"props": {
"label": "Save Changes",
"variant": "primary",
"action": "emit:save"
}
}
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Button text |
variant | 'primary' | 'outline' | 'ghost' | 'outline' | Visual style |
action | string | — | Action string to emit on click |
disabled | boolean | false | Disable the button |
TextField
Single-line text input with a label.
{
"type": "TextField",
"props": {
"label": "Email",
"placeholder": "you@example.com",
"value": "jane@ainative.studio"
}
}
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Label displayed above the input |
placeholder | string | — | Placeholder text |
value | string | '' | Current value |
type | 'text' | 'email' | 'password' | 'number' | 'text' | Input type |
onChange | string | — | Action string or JSON Pointer to update on change |
CheckBox
Boolean toggle with a label.
{
"type": "CheckBox",
"props": {
"label": "Subscribe to newsletter",
"checked": true
}
}
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | required | Label text |
checked | boolean | false | Current checked state |
onChange | string | — | Action string or JSON Pointer to update on change |
Slider
Numeric range slider.
{
"type": "Slider",
"props": {
"label": "Temperature",
"min": 0,
"max": 2,
"value": 0.7,
"step": 0.1
}
}
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Label displayed above the slider |
min | number | 0 | Minimum value |
max | number | 100 | Maximum value |
value | number | 0 | Current value |
step | number | 1 | Step increment |
onChange | string | — | Action string or JSON Pointer to update on change |
Tabs
Horizontal tab bar. Displays the active tab visually; switching tabs fires an action.
{
"type": "Tabs",
"props": {
"tabs": [
{ "id": "overview", "label": "Overview" },
{ "id": "settings", "label": "Settings" }
],
"activeTab": "overview"
}
}
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | Array<{ id: string; label: string }> | required | Tab definitions |
activeTab | string | — | ID of the currently active tab |
onChange | string | — | Action string fired when a tab is selected |
List
Vertical list of child components with optional bordered styling.
{
"type": "List",
"props": { "variant": "bordered" },
"children": [
{ "type": "Text", "children": "Item one" },
{ "type": "Text", "children": "Item two" }
]
}
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'default' | 'bordered' | 'default' | bordered adds a border and dividers between items |
children | A2UIComponent[] | required | List items |
ChoicePicker
Single-select radio group with labeled options and optional descriptions.
{
"type": "ChoicePicker",
"props": {
"options": [
{ "id": "fast", "label": "Fast", "description": "Lower quality, faster response" },
{ "id": "balanced", "label": "Balanced", "description": "Recommended" },
{ "id": "quality", "label": "Quality", "description": "Best output, slower" }
],
"selected": "balanced"
}
}
| Prop | Type | Default | Description |
|---|---|---|---|
options | Array<{ id: string; label: string; description?: string }> | required | Available choices |
selected | string | — | ID of the currently selected option |
onChange | string | — | Action string or JSON Pointer to update on selection |
Divider
Horizontal rule used to visually separate sections.
{ "type": "Divider" }
No props.
Planned components (6)
The following components are defined in the A2UI protocol specification but are not yet implemented in the React renderer. They are rendered as a debug placeholder in development.
| Type | Description |
|---|---|
Icon | Named icon from a configurable icon set |
Image | Image with src, alt, and optional dimensions |
Video | Video player with controls |
AudioPlayer | Audio player with playback controls |
Modal | Dialog overlay triggered by an action |
DateInput | Date and time picker |
Integration guide
Basic usage
import { A2UIRenderer } from '@ainative/ai-kit-a2ui';
const spec = {
type: 'Container',
props: { direction: 'column', gap: 16, padding: 24 },
children: [
{ type: 'Heading', props: { level: 2 }, children: 'Hello, Agent UI' },
{ type: 'Text', children: 'This entire UI was generated from JSON.' },
{ type: 'Button', props: { label: 'Get Started', variant: 'primary', action: 'emit:start' } },
],
};
export function AgentOutput() {
return (
<A2UIRenderer
spec={spec}
onAction={(action) => {
console.log('User triggered:', action);
}}
/>
);
}
With WebSocket (live agent)
import { A2UIRenderer } from '@ainative/ai-kit-a2ui';
import { useCoAgent } from '@ainative/ai-kit-a2ui/react';
export function LiveAgentUI() {
const { schema, sendAction } = useCoAgent({
url: 'wss://api.ainative.studio/agent/ws',
agentId: 'my-agent',
});
if (!schema) return <p>Connecting to agent…</p>;
return (
<A2UIRenderer
spec={schema}
onAction={(action) => sendAction(action)}
/>
);
}
Custom component registry
Override default components or add your own:
import { A2UIRenderer } from '@ainative/ai-kit-a2ui';
import { MyChart } from '@/components/MyChart';
export function CustomAgentUI({ spec }) {
return (
<A2UIRenderer
spec={spec}
components={{
// Override a built-in component
Button: ({ label, variant, onClick }) => (
<button className={`btn btn-${variant}`} onClick={onClick}>
{label}
</button>
),
// Add a custom component type the agent can use
MetricsChart: MyChart,
}}
onAction={(action) => console.log(action)}
/>
);
}
Full JSON examples
User profile form
{
"type": "Container",
"props": { "direction": "column", "gap": 16, "padding": 24 },
"children": [
{ "type": "Heading", "props": { "level": 2 }, "children": "Edit Profile" },
{
"type": "Text",
"props": { "variant": "muted" },
"children": "Update your personal information below."
},
{ "type": "Divider" },
{
"type": "TextField",
"props": {
"label": "Full Name",
"placeholder": "John Doe",
"value": "Jane Smith"
}
},
{
"type": "TextField",
"props": {
"label": "Email",
"placeholder": "you@example.com",
"value": "jane@ainative.studio",
"type": "email"
}
},
{
"type": "Slider",
"props": { "label": "Experience Level", "min": 1, "max": 10, "value": 7, "step": 1 }
},
{
"type": "CheckBox",
"props": { "label": "Subscribe to newsletter", "checked": true }
},
{
"type": "Button",
"props": { "variant": "primary", "label": "Save Changes", "action": "emit:save" }
}
]
}
Agent status dashboard
{
"type": "Container",
"props": { "direction": "column", "gap": 16, "padding": 24 },
"children": [
{ "type": "Heading", "props": { "level": 2 }, "children": "Agent Status" },
{
"type": "Tabs",
"props": {
"tabs": [
{ "id": "active", "label": "Active (3)" },
{ "id": "idle", "label": "Idle (2)" },
{ "id": "errors", "label": "Errors (0)" }
],
"activeTab": "active"
}
},
{
"type": "List",
"props": { "variant": "bordered" },
"children": [
{ "type": "Text", "children": "aurora — Processing customer query (45s)" },
{ "type": "Text", "children": "sage — Generating API response (12s)" },
{ "type": "Text", "children": "nova — Running security scan (78s)" }
]
},
{ "type": "Divider" },
{
"type": "Container",
"props": { "direction": "row", "gap": 8 },
"children": [
{ "type": "Button", "props": { "variant": "primary", "label": "Refresh", "action": "ws:refresh" } },
{ "type": "Button", "props": { "variant": "outline", "label": "View Logs", "action": "navigate:/logs" } }
]
}
]
}
Model configuration picker
{
"type": "Container",
"props": { "direction": "column", "gap": 16, "padding": 24 },
"children": [
{ "type": "Heading", "props": { "level": 2 }, "children": "Select AI Model" },
{
"type": "Text",
"props": { "variant": "muted" },
"children": "Choose the model that best fits your use case."
},
{
"type": "ChoicePicker",
"props": {
"options": [
{ "id": "llama-70b", "label": "Llama 3.3 70B", "description": "Best open-source quality" },
{ "id": "llama-8b", "label": "Llama 3.3 8B", "description": "Fast and cost-efficient" },
{ "id": "mistral", "label": "Mistral 7B", "description": "Compact and capable" }
],
"selected": "llama-70b"
}
},
{
"type": "Slider",
"props": { "label": "Temperature", "min": 0, "max": 2, "value": 0.7, "step": 0.1 }
},
{
"type": "Slider",
"props": { "label": "Max Tokens", "min": 100, "max": 4000, "value": 2000, "step": 100 }
},
{
"type": "CheckBox",
"props": { "label": "Enable streaming", "checked": true }
},
{
"type": "Button",
"props": { "variant": "primary", "label": "Apply Configuration", "action": "emit:applyConfig" }
}
]
}
Packages
| Package | Status | Description |
|---|---|---|
@ainative/ai-kit-a2ui-core | Alpha | Framework-agnostic core. Protocol types, JSON Pointer (RFC 6901), WebSocket transport, component registry. Zero dependencies. 429 tests, 96% coverage |
@ainative/ai-kit-a2ui | Alpha | React renderer with shadcn/ui component mappings. 11 of 17 components implemented. 70 tests passing |
@ainative/ai-kit-nextjs-a2ui | Planned | Next.js renderer with Server Components, Server Actions, and Streaming SSR |
# Core (framework-agnostic)
npm install @ainative/ai-kit-a2ui-core
# React renderer
npm install @ainative/ai-kit-a2ui
A2UIRenderer props
| Name | Type | Default | Description |
|---|---|---|---|
spec | A2UIComponent | required | The root A2UI component spec to render |
onAction | (action: string) => void | — | Callback fired when the user triggers any action (button click, etc.) |
components | Record<string, React.ComponentType> | — | Override or extend the default component registry |
state | Record<string, unknown> | {} | Initial state object for JSON Pointer bindings |
onStateChange | (state: Record<string, unknown>) => void | — | Callback fired when state is updated by user interaction |
TypeScript types
import type {
A2UIComponent,
A2UIComponentRegistry,
A2UIAction,
A2UIState,
} from '@ainative/ai-kit-a2ui-core';
See the source repository at github.com/AINative-Studio/ai-kit-a2ui for full type definitions and the protocol specification.
Further reading
- Google A2UI specification
- AI Kit React components —
StreamingMessage,CodeBlock,StreamingIndicator - React hooks —
useChat,useAgent,useTask,useMemory - AI Kit overview