@a2ui/react
v0.9.0-alpha.0
Published
React renderer for A2UI (Agent-to-User Interface)
Readme
@a2ui/react
React renderer for A2UI (Agent-to-User Interface) - enables AI agents to generate rich, interactive user interfaces through declarative JSON.
Table of Contents
- Installation
- Quick Start
- Architecture
- Components
- Hooks
- Theme System
- Component Registry
- Styles
- Development
- Visual Parity Testing
- API Reference
- Contributing
Installation
npm install @a2ui/reactPeer Dependencies: - React 18.x or 19.x - React DOM 18.x or 19.x
Quick Start
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
}Standalone Viewer
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)}
/>
);
}Architecture
Versioned Structure
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/indexBackward 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.
Two-Context Pattern
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?
- A2UIActionsContext: Contains stable action callbacks that never change
reference. Components using
useA2UIActions()won't re-render when data changes. - A2UIStateContext: Contains a version counter that increments on state changes. Only components that need to react to data changes subscribe to this.
This separation prevents unnecessary re-renders and provides fine-grained control over component updates.
Data Flow
Agent Server React App
│ │
│ ServerToClientMessage[] │
├─────────────────────────────▶│ processMessages()
│ │
│ ▼
│ ┌────────────────┐
│ │ MessageProcessor│
│ │ (surfaces) │
│ └───────┬────────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ A2UIRenderer │
│ │ (per surface) │
│ └───────┬────────┘
│ │
│ ▼
│ ┌────────────────┐
│ │ ComponentNode │
│ │ (recursive) │
│ └───────┬────────┘
│ │
│ A2UIClientEventMessage │ User interaction
│◀────────────────────────────┤ dispatch()
│ │Components
All components are wrapped with React.memo() for performance optimization.
Content Components
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
Layout Components
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
Interactive Components
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
Component Structure
Each component mirrors the Lit renderer's Shadow DOM structure for visual parity:
// React component structure
<div className="a2ui-{component}"> {/* :host equivalent */}
<section className="theme-classes"> {/* internal element */}
{children} {/* ::slotted(*) equivalent */}
</section>
</div>Hooks
useA2UI()
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>;
}useA2UIActions()
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>;
}useA2UIState()
Subscribe to state changes (triggers re-renders):
import { useA2UIState } from '@a2ui/react';
function VersionDisplay() {
const { version } = useA2UIState();
return <span>State version: {version}</span>;
}useA2UIContext()
Combined access to actions and state:
import { useA2UIContext } from '@a2ui/react';
function MyComponent() {
const { processMessages, dispatch, version } = useA2UIContext();
// ...
}useA2UIComponent()
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.
Theme System
Using the Default Theme
import { A2UIProvider, litTheme } from '@a2ui/react';
<A2UIProvider theme={litTheme}>
{/* components */}
</A2UIProvider>Creating a Custom Theme
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}>
{/* components */}
</A2UIProvider>Theme Structure
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
};
}Component Registry
Default Catalog
The default catalog registers all standard A2UI components:
import { initializeDefaultCatalog } from '@a2ui/react';
// Call once at app startup
initializeDefaultCatalog();Custom Components
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,
});Lazy Loading
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.
Styles
Injecting Styles
Inject A2UI structural and component styles once at app startup:
import { injectStyles } from '@a2ui/react/styles';
// In your app entry point
injectStyles();Style Architecture
The styles module provides:
- structuralStyles: Utility classes from Lit renderer (layout-, typography-, color-*)
- componentSpecificStyles: CSS that replicates Lit's Shadow DOM scoped styles
import { structuralStyles, componentSpecificStyles } from '@a2ui/react/styles';CSS Variables
CSS color variables must be defined by the host application:
:root {
--n-0: #ffffff;
--n-100: #f5f5f5;
/* ... other palette variables */
--p-500: #3b82f6;
/* ... */
}Removing Styles
For cleanup (e.g., in tests):
import { removeStyles } from '@a2ui/react/styles';
removeStyles();Development
Setup
cd renderers/react
npm installBuild
npm run build # Build the package
npm run dev # Watch modeType Check
npm run typecheckLint
npm run lintTesting
Unit Tests
Uses Vitest + React Testing Library.
npm test # Run once
npm run test:watch # Watch modeStructure: 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( );
fireEvent.click(screen.getByRole('button')); expect(onAction).toHaveBeenCalled(); }); ```
Visual Parity Testing
The React renderer maintains visual parity with the Lit renderer (reference implementation). A comprehensive test suite compares pixel-perfect screenshots between both renderers.
Running Visual Parity Tests
cd visual-parity
npm install
npm testQuick Commands
# 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:5002Documentation
- visual-parity/README.md - Test suite usage and fixture creation
- visual-parity/PARITY.md - CSS transformation approach and implementation status
Key Concepts
- Structural Mirroring: React components mirror Lit's Shadow DOM structure
- CSS Selector Transformation:
:host→.a2ui-surface .a2ui-{component} - Specificity Matching: Uses
:where()to match Lit's low specificity
API Reference
Core Exports
// 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 };Types
import type {
Types,
Theme,
Surface,
SurfaceID,
AnyComponentNode,
ServerToClientMessage,
A2UIClientEventMessage,
A2UIComponentProps,
A2UIProviderProps,
A2UIRendererProps,
UseA2UIResult,
UseA2UIComponentResult,
} from '@a2ui/react';Contributing
Code Style
- All components use
React.memo()for performance - Use the two-context pattern for state management
- Follow the existing component structure for visual parity
Adding a New Component
- Create component in
src/components/{category}/{ComponentName}.tsx - Follow wrapper div + section structure (see Component Structure)
- Register in
src/registry/defaultCatalog.ts - Export from
src/index.ts - Add unit tests in
tests/components/{ComponentName}.test.tsx - Add visual parity fixtures in
visual-parity/fixtures/components/
