renderers/react/README.md
React renderer for A2UI (Agent-to-User Interface) - enables AI agents to generate rich, interactive user interfaces through declarative JSON.
npm install @a2ui/react
Peer Dependencies: - React 18.x or 19.x - React DOM 18.x or 19.x
import { A2UIProvider, A2UIRenderer, injectStyles } from '@a2ui/react';
import { useA2UI } from '@a2ui/react';
// Inject A2UI styles once at app startup
injectStyles();
function App() {
const { processMessages } = useA2UI();
// Process A2UI messages from your AI agent
const handleAgentResponse = (messages) => {
processMessages(messages);
};
return (
<A2UIProvider onAction={handleAction}>
<A2UIRenderer surfaceId="main" />
</A2UIProvider>
);
}
// Handle user interactions
function handleAction(message) {
console.log('User action:', message);
// Send to your AI agent backend
}
For simpler use cases, use the all-in-one A2UIViewer:
import { A2UIViewer, injectStyles } from '@a2ui/react';
injectStyles();
function App() {
const messages = [...]; // A2UI messages from agent
return (
<A2UIViewer
messages={messages}
onAction={(msg) => console.log('Action:', msg)}
/>
);
}
To support the evolution of the A2UI protocol, the @a2ui/react package is organized into versioned subdirectories (e.g., v0_8/, and soon v0_9/).
renderers/react/
├── src/
│ ├── v0_8/ # Implementation of the v0.8 protocol
│ │ ├── components/
│ │ ├── core/
│ │ └── ...
│ ├── index.ts # Proxy export for backward compatibility -> ./v0_8/index
│ ├── types.ts # Proxy export -> ./v0_8/types
│ └── styles/ # Proxy export -> ../v0_8/styles/index
Backward Compatibility & Future Roadmap
The top-level exports in package.json and the proxy files in the src/ root currently default to the v0_8 implementation. This guarantees that existing applications relying on import { A2UIProvider } from '@a2ui/react' will continue to work without modification.
Applications can also explicitly import from a specific version path if desired (e.g., import { A2UIProvider } from '@a2ui/react/v0_8').
As the v0_9 protocol implementation is added to the v0_9/ subdirectory, the top-level exports will be updated to point to the newest stable version.
Note: This side-by-side versioned directory structure is a temporary transition phase. Eventually, the v0_8 renderer will be deprecated and removed. The package will then revert to a single, unified codebase that natively supports multiple versions of the A2UI specification simultaneously.
The React renderer uses a two-context architecture for optimal performance:
┌─────────────────────────────────────────────────────────┐
│ A2UIProvider │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────┐ ┌─────────────────────────┐ │
│ │ A2UIActionsContext │ │ A2UIStateContext │ │
│ │ (stable reference) │ │ (triggers re-renders) │ │
│ │ │ │ │ │
│ │ • processMessages │ │ • version │ │
│ │ • setData │ │ │ │
│ │ • dispatch │ │ │ │
│ │ • getData │ │ │ │
│ │ • getSurface │ │ │ │
│ └─────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ ThemeProvider │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────────────────┐ │ │
│ │ │ A2UIRenderer│───▶│ ComponentNode │ │ │
│ │ │ (surfaceId) │ │ (recursive rendering) │ │ │
│ │ └─────────────┘ └─────────────────────────┘ │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Why two contexts?
useA2UIActions() won't re-render when data
changes.This separation prevents unnecessary re-renders and provides fine-grained control over component updates.
Agent Server React App
│ │
│ ServerToClientMessage[] │
├─────────────────────────────▶│ processMessages()
│ │
│ ▼
│ ┌────────────────┐
│ │ MessageProcessor│
│ │ (surfaces) │
│ └───────┬────────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ A2UIRenderer │
│ │ (per surface) │
│ └───────┬────────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ ComponentNode │
│ │ (recursive) │
│ └───────┬────────┘
│ │
│ A2UIClientEventMessage │ User interaction
│◀────────────────────────────┤ dispatch()
│ │
All components are wrapped with React.memo() for performance optimization.
| Component | Description |
|---|---|
Text | Renders text with markdown support |
Image | Displays images with various usage hints |
Icon | Renders Material Symbols icons |
Divider | Horizontal or vertical divider |
Video | Video player |
AudioPlayer | Audio player |
| Component | Description |
|---|---|
Column | Vertical flex container |
Row | Horizontal flex container |
Card | Card container with styling |
List | List container (vertical/horizontal) |
Tabs | Tabbed interface |
Modal | Modal dialog |
| Component | Description |
|---|---|
Button | Clickable button with action dispatch |
TextField | Text input (single/multiline) |
CheckBox | Checkbox input |
Slider | Range slider |
DateTimeInput | Date/time picker |
MultipleChoice | Radio/checkbox group |
Each component mirrors the Lit renderer's Shadow DOM structure for visual parity:
// React component structure
<div className="a2ui-{component}">
<section className="theme-classes">
{children}
</section>
</div>
High-level hook for external application use:
import { useA2UI } from '@a2ui/react';
function MyComponent() {
const { processMessages, clearSurfaces } = useA2UI();
const loadUI = async () => {
const response = await fetch('/api/agent');
const messages = await response.json();
processMessages(messages);
};
return <button onClick={loadUI}>Load UI</button>;
}
Access stable actions without triggering re-renders:
import { useA2UIActions } from '@a2ui/react';
function ActionButton() {
const { dispatch } = useA2UIActions();
const handleClick = () => {
dispatch({
event: { action: { name: 'submit' } },
sourceComponent: 'button-1',
surfaceId: 'main',
});
};
return <button onClick={handleClick}>Submit</button>;
}
Subscribe to state changes (triggers re-renders):
import { useA2UIState } from '@a2ui/react';
function VersionDisplay() {
const { version } = useA2UIState();
return <span>State version: {version}</span>;
}
Combined access to actions and state:
import { useA2UIContext } from '@a2ui/react';
function MyComponent() {
const { processMessages, dispatch, version } = useA2UIContext();
// ...
}
Internal hook for component implementations. Automatically subscribes to state changes so components with path bindings re-render when data updates.
import { useA2UIComponent } from '@a2ui/react';
function CustomComponent({ node, surfaceId }) {
const {
theme,
resolveString,
resolveNumber,
resolveBoolean,
setValue,
getValue,
sendAction,
getUniqueId,
} = useA2UIComponent(node, surfaceId);
const text = resolveString(node.properties.text);
// ...
}
Path Binding Reactivity: When a component uses setValue() to update the
data model, all components reading from the same path via resolveString(),
resolveNumber(), or resolveBoolean() will automatically re-render with the
new value.
import { A2UIProvider, litTheme } from '@a2ui/react';
<A2UIProvider theme={litTheme}>
</A2UIProvider>
import { litTheme } from '@a2ui/react';
const customTheme = {
...litTheme,
components: {
...litTheme.components,
Button: {
...litTheme.components.Button,
all: {
'my-button-class': true,
'rounded-lg': true,
},
},
},
};
<A2UIProvider theme={customTheme}>
</A2UIProvider>
interface Theme {
components: {
[ComponentName]: {
all: ClassMap; // Always applied
[variant]: ClassMap; // Variant-specific (e.g., primary, secondary)
};
};
elements: {
[elementName]: ClassMap; // HTML element styling
};
markdown: {
[tagName]: string[]; // Markdown element classes
};
additionalStyles?: {
[ComponentName]: Record<string, string>; // Inline styles
};
}
The default catalog registers all standard A2UI components:
import { initializeDefaultCatalog } from '@a2ui/react';
// Call once at app startup
initializeDefaultCatalog();
Register custom components to extend or override the default catalog:
import { ComponentRegistry } from '@a2ui/react';
// Get the singleton registry
const registry = ComponentRegistry.getInstance();
// Register a custom component
registry.register('CustomButton', {
component: MyCustomButton,
});
// Override an existing component
registry.register('Button', {
component: MyEnhancedButton,
});
Components can be lazy-loaded for code splitting:
registry.register('HeavyChart', {
component: () => import('./components/HeavyChart'),
lazy: true,
});
Note: Small, commonly-used components (like Tabs, Modal) should be statically imported to avoid Vite cache issues during development.
Inject A2UI structural and component styles once at app startup:
import { injectStyles } from '@a2ui/react/styles';
// In your app entry point
injectStyles();
The styles module provides:
import { structuralStyles, componentSpecificStyles } from '@a2ui/react/styles';
CSS color variables must be defined by the host application:
:root {
--n-0: #ffffff;
--n-100: #f5f5f5;
/* ... other palette variables */
--p-500: #3b82f6;
/* ... */
}
For cleanup (e.g., in tests):
import { removeStyles } from '@a2ui/react/styles';
removeStyles();
cd renderers/react
npm install
npm run build # Build the package
npm run dev # Watch mode
npm run typecheck
npm run lint
Uses Vitest + React Testing Library.
npm test # Run once
npm run test:watch # Watch mode
Structure: tests/ ├── setup.ts # Initializes component catalog ├── helpers.tsx # TestWrapper, TestRenderer, message creators └── components/ # Component tests (*.test.tsx)
Example: ```tsx import { render, screen, fireEvent } from '@testing-library/react'; import { TestWrapper, TestRenderer, createSimpleMessages } from '../helpers';
it('should dispatch action on click', () => { const onAction = vi.fn(); const messages = createSimpleMessages('btn-1', 'Button', { child: 'text-1', action: { name: 'submit' }, });
render( <TestWrapper onAction={onAction}> <TestRenderer messages={messages} /> </TestWrapper> );
fireEvent.click(screen.getByRole('button')); expect(onAction).toHaveBeenCalled(); }); ```
The React renderer maintains visual parity with the Lit renderer (reference implementation). A comprehensive test suite compares pixel-perfect screenshots between both renderers.
cd visual-parity
npm install
npm test
# Run all tests
npm test
# Run specific component tests
npm test -- --grep "button"
# Run with UI mode
npm run test:ui
# Start dev servers for manual inspection
npm run dev
# React: http://localhost:5001
# Lit: http://localhost:5002
:host → .a2ui-surface .a2ui-{component}:where() to match Lit's low specificity// Provider and Renderer
export { A2UIProvider, A2UIRenderer, A2UIViewer, ComponentNode };
// Hooks
export { useA2UI, useA2UIActions, useA2UIState, useA2UIContext, useA2UIComponent };
// Registry
export { ComponentRegistry, registerDefaultCatalog, initializeDefaultCatalog };
// Theme
export { ThemeProvider, useTheme, litTheme, defaultTheme };
// Styles (from '@a2ui/react/styles')
export { injectStyles, removeStyles, structuralStyles, componentSpecificStyles };
// Utilities
export { cn, classMapToString, stylesToObject };
// All component exports
export { Text, Image, Icon, Divider, Video, AudioPlayer };
export { Row, Column, Card, List, Tabs, Modal };
export { Button, TextField, CheckBox, Slider, DateTimeInput, MultipleChoice };
import type {
Types,
Theme,
Surface,
SurfaceID,
AnyComponentNode,
ServerToClientMessage,
A2UIClientEventMessage,
A2UIComponentProps,
A2UIProviderProps,
A2UIRendererProps,
UseA2UIResult,
UseA2UIComponentResult,
} from '@a2ui/react';
React.memo() for performancesrc/components/{category}/{ComponentName}.tsxsrc/registry/defaultCatalog.tssrc/index.tstests/components/{ComponentName}.test.tsxvisual-parity/fixtures/components/