Guide 1 of 9

Render A2UI in React

Install the two npm packages, connect a host, paint a surface with <HostSurface>.

Install

Two packages: @exponential-at/ui (the catalog, themes, reducer and host runtime) and @exponential-at/ui-react (the renderer). React 18 or 19; the renderer ships its own styles, so there is no stylesheet or Tailwind setup.

shell
npm install @exponential-at/ui @exponential-at/ui-react
Not on the registries yetNothing is published yet: every package publishes on the first ui-v* tag. Until then, pack the tarballs from a checkout of the repo and install those:
shell
bun samples/exponential-ui/pack-local.ts      # → samples/exponential-ui/.packs/*.tgz
npm install ../exponential/samples/exponential-ui/.packs/exponential-at-ui-0.1.0.tgz \
            ../exponential/samples/exponential-ui/.packs/exponential-at-ui-react-0.1.0.tgz
guides/react/package.jsonthe whole dependency list
json
{
  "name": "exponential-ui-guide-react",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc --noEmit && vite build"
  },
  "dependencies": {
    "@exponential-at/ui": "^0.1.0",
    "@exponential-at/ui-react": "^0.1.0",
    "react": "^19.2.0",
    "react-dom": "^19.2.0"
  },
  "devDependencies": {
    "@types/react": "^19.2.0",
    "@types/react-dom": "^19.2.0",
    "typescript": "^5.7.2",
    "vite": "^7.1.0"
  }
}

Paint a surface

One ExponentialHost per app: it reads A2UI messages from a transport, routes them to surfaces and posts the user's actions back. <HostSurface> paints one surface by id, in a theme and a mode, with a fallback until the agent has created it.

guides/react/src/App.tsx
tsx
import { useEffect, useMemo } from "react"
import { ExponentialHost, JsonlStreamTransport } from "@exponential-at/ui"
import { HostSurface } from "@exponential-at/ui-react"

export function App() {
  // One host per app: A2UI messages in over a transport, actions back out.
  const host = useMemo(
    () => new ExponentialHost({ transport: new JsonlStreamTransport({ url: `/a2ui.jsonl`, postUrl: `/action` }) }),
    []
  )
  useEffect(() => {
    host.connect()
    return () => host.close()
  }, [host])

  // Paints the surface the agent creates; the theme is data, the mode a prop.
  return <HostSurface host={host} surfaceId="main" theme="exponential" mode="dark" fallback={<p>Waiting for the agent…</p>} />
}

The transport here reads a JSONL stream and posts actions to /action. Swap in SseTransport, WebSocketTransport, McpTransport (A2UI inside MCP tool results) or MemoryTransport (you push the messages) without touching the surface. theme takes a built-in id, a theme JSON or a resolved theme.

Without a host

When you already have the messages (a chat transcript, a test), skip the host: useSurface reduces messages into a tree and <ExponentialSurface> paints it. The plugin object takes icons, onAction, onInput, openUrl and extra functions.

tsx
const surface = useSurface({ surfaceId: "main" })
useEffect(() => messages.forEach(surface.apply), [messages])
return <ExponentialSurface surface={surface} theme="neutral" mode="light" host={{ onAction }} />

More in the React renderer's README: how a theme becomes scoped CSS, overlays, windowed lists and painter overrides.

Compiled in CI

Every file on this page is a real file in apps/ui-site/guides/react. CI copies it into an empty directory and builds it from scratch, the way you would, so the example cannot drift from the SDK:

shell
bun apps/ui-site/guides/check.ts react

For a complete app on this platform, see the samples: four hosts rendering the same surface from a local A2UI server, in a third-party theme, with a custom extension component.

Next guideRender A2UI in SwiftUI