Back to A2ui

@a2ui/react

renderers/react/README.md

0.917.8 KB
Original Source

@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

bash
npm install @a2ui/react

Peer Dependencies: - React 18.x or 19.x - React DOM 18.x or 19.x

Quick Start

tsx
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:

tsx
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/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.

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

ComponentDescription
TextRenders text with markdown support
ImageDisplays images with various usage hints
IconRenders Material Symbols icons
DividerHorizontal or vertical divider
VideoVideo player
AudioPlayerAudio player

Layout Components

ComponentDescription
ColumnVertical flex container
RowHorizontal flex container
CardCard container with styling
ListList container (vertical/horizontal)
TabsTabbed interface
ModalModal dialog

Interactive Components

ComponentDescription
ButtonClickable button with action dispatch
TextFieldText input (single/multiline)
CheckBoxCheckbox input
SliderRange slider
DateTimeInputDate/time picker
MultipleChoiceRadio/checkbox group

Component Structure

Each component mirrors the Lit renderer's Shadow DOM structure for visual parity:

tsx
// React component structure
<div className="a2ui-{component}">
  <section className="theme-classes">
    {children}
  </section>
</div>

Hooks

useA2UI()

High-level hook for external application use:

tsx
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:

tsx
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):

tsx
import { useA2UIState } from '@a2ui/react';

function VersionDisplay() {
  const { version } = useA2UIState();
  return <span>State version: {version}</span>;
}

useA2UIContext()

Combined access to actions and state:

tsx
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.

tsx
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

tsx
import { A2UIProvider, litTheme } from '@a2ui/react';

<A2UIProvider theme={litTheme}>
</A2UIProvider>

Creating a Custom Theme

tsx
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>

Theme Structure

typescript
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:

tsx
import { initializeDefaultCatalog } from '@a2ui/react';

// Call once at app startup
initializeDefaultCatalog();

Custom Components

Register custom components to extend or override the default catalog:

tsx
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:

tsx
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:

tsx
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
typescript
import { structuralStyles, componentSpecificStyles } from '@a2ui/react/styles';

CSS Variables

CSS color variables must be defined by the host application:

css
:root {
  --n-0: #ffffff;
  --n-100: #f5f5f5;
  /* ... other palette variables */
  --p-500: #3b82f6;
  /* ... */
}

Removing Styles

For cleanup (e.g., in tests):

tsx
import { removeStyles } from '@a2ui/react/styles';

removeStyles();

Development

Setup

bash
cd renderers/react
npm install

Build

bash
npm run build    # Build the package
npm run dev      # Watch mode

Type Check

bash
npm run typecheck

Lint

bash
npm run lint

Testing

Unit Tests

Uses Vitest + React Testing Library.

bash
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(); }); ```

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

bash
cd visual-parity
npm install
npm test

Quick Commands

bash
# 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

Documentation

Key Concepts

  1. Structural Mirroring: React components mirror Lit's Shadow DOM structure
  2. CSS Selector Transformation: :host.a2ui-surface .a2ui-{component}
  3. Specificity Matching: Uses :where() to match Lit's low specificity

API Reference

Core Exports

typescript
// 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

typescript
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

  1. Create component in src/components/{category}/{ComponentName}.tsx
  2. Follow wrapper div + section structure (see Component Structure)
  3. Register in src/registry/defaultCatalog.ts
  4. Export from src/index.ts
  5. Add unit tests in tests/components/{ComponentName}.test.tsx
  6. Add visual parity fixtures in visual-parity/fixtures/components/