Skip to main content

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
note

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
  1. Your agent responds with a JSON object following the A2UI component schema.
  2. Pass that JSON to <A2UIRenderer>. It validates the spec and maps each type to a component from the registry.
  3. The default registry uses shadcn/ui components with full accessibility out of the box.
  4. User interactions (button clicks, text input, slider changes) are captured and sent back to the agent via the onAction callback or WebSocket transport.
  5. 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:

PatternExampleMeaning
navigate:/pathnavigate:/dashboardNavigate to a route
set:/state/keyset:/state/isOpenWrite a value to state
emit:eventNameemit:formSubmitFire a named event to the onAction handler
ws:methodNamews:runAgentSend 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": []
}
PropTypeDefaultDescription
direction'row' | 'column''column'Flex direction
gapnumber0Gap between children in pixels
paddingnumber0Internal padding in pixels

Heading

Section heading. Renders the appropriate <h1><h6> element.

{
"type": "Heading",
"props": { "level": 2 },
"children": "Edit Profile"
}
PropTypeDefaultDescription
level1 | 2 | 3 | 4 | 5 | 62Heading level
childrenstringrequiredHeading text

Text

Paragraph text with optional visual variants.

{
"type": "Text",
"props": { "variant": "muted" },
"children": "Some descriptive copy here."
}
PropTypeDefaultDescription
variant'default' | 'muted''default'muted renders in a subdued color for secondary text
childrenstringrequiredText content

Button

Clickable action button. Fires the action string on click.

{
"type": "Button",
"props": {
"label": "Save Changes",
"variant": "primary",
"action": "emit:save"
}
}
PropTypeDefaultDescription
labelstringrequiredButton text
variant'primary' | 'outline' | 'ghost''outline'Visual style
actionstringAction string to emit on click
disabledbooleanfalseDisable the button

TextField

Single-line text input with a label.

{
"type": "TextField",
"props": {
"label": "Email",
"placeholder": "you@example.com",
"value": "jane@ainative.studio"
}
}
PropTypeDefaultDescription
labelstringLabel displayed above the input
placeholderstringPlaceholder text
valuestring''Current value
type'text' | 'email' | 'password' | 'number''text'Input type
onChangestringAction string or JSON Pointer to update on change

CheckBox

Boolean toggle with a label.

{
"type": "CheckBox",
"props": {
"label": "Subscribe to newsletter",
"checked": true
}
}
PropTypeDefaultDescription
labelstringrequiredLabel text
checkedbooleanfalseCurrent checked state
onChangestringAction 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
}
}
PropTypeDefaultDescription
labelstringLabel displayed above the slider
minnumber0Minimum value
maxnumber100Maximum value
valuenumber0Current value
stepnumber1Step increment
onChangestringAction 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"
}
}
PropTypeDefaultDescription
tabsArray<{ id: string; label: string }>requiredTab definitions
activeTabstringID of the currently active tab
onChangestringAction 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" }
]
}
PropTypeDefaultDescription
variant'default' | 'bordered''default'bordered adds a border and dividers between items
childrenA2UIComponent[]requiredList 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"
}
}
PropTypeDefaultDescription
optionsArray<{ id: string; label: string; description?: string }>requiredAvailable choices
selectedstringID of the currently selected option
onChangestringAction 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.

TypeDescription
IconNamed icon from a configurable icon set
ImageImage with src, alt, and optional dimensions
VideoVideo player with controls
AudioPlayerAudio player with playback controls
ModalDialog overlay triggered by an action
DateInputDate 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

PackageStatusDescription
@ainative/ai-kit-a2ui-coreAlphaFramework-agnostic core. Protocol types, JSON Pointer (RFC 6901), WebSocket transport, component registry. Zero dependencies. 429 tests, 96% coverage
@ainative/ai-kit-a2uiAlphaReact renderer with shadcn/ui component mappings. 11 of 17 components implemented. 70 tests passing
@ainative/ai-kit-nextjs-a2uiPlannedNext.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

NameTypeDefaultDescription
specA2UIComponentrequiredThe root A2UI component spec to render
onAction(action: string) => voidCallback fired when the user triggers any action (button click, etc.)
componentsRecord<string, React.ComponentType>Override or extend the default component registry
stateRecord<string, unknown>{}Initial state object for JSON Pointer bindings
onStateChange(state: Record<string, unknown>) => voidCallback 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