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.
npm install @exponential-at/ui @exponential-at/ui-reactui-v* tag. Until then, pack the tarballs from a checkout of the repo and install those: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{
"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.
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.
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:
bun apps/ui-site/guides/check.ts reactFor 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.