Render A2UI in React
Install
Section titled “Install”Two packages: @exponential_app/eui (the catalog, themes, reducer and host runtime) and @exponential_app/eui-react (the renderer). It needs React 19. The renderer ships its own styles, so a stylesheet or Tailwind setup is unnecessary.
npm install @exponential_app/eui @exponential_app/eui-reactThe guide project’s whole dependency list:
{ "name": "exponential-ui-guide-react", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build" }, "dependencies": { "@exponential_app/eui": "^0.12.0", "@exponential_app/eui-react": "^0.12.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
Section titled “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_app/eui"import { HostSurface } from "@exponential_app/eui-react"import { Button } from "@exponential_app/eui-react/primitives"import "@exponential_app/eui-react/styles.css"
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. // Below it, a primitive from the same set (the stylesheet above styles it). return ( <> <HostSurface host={host} surfaceId="main" theme="exponential" mode="dark" fallback={<p>Waiting for the agent…</p>} /> <Button variant="outline" onClick={() => { host.close(); host.connect() }}> Reconnect </Button> </> )}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) and the surface stays as it is. theme takes a built-in id, a theme JSON or a resolved theme.
Without a host
Section titled “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 }} />The React renderer’s README covers how a theme becomes scoped CSS, overlays, windowed lists and painter overrides.
Primitives
Section titled “Primitives”The app around a surface can use the same components: @exponential_app/eui-react/primitives is the shadcn set on Radix (button, dialog, select, table, command, toast, calendar and more). Import its compiled stylesheet once. Inside a surface they wear its theme; outside one, globalsCss(theme) from @exponential_app/eui/web writes the theme as shadcn variables.
import "@exponential_app/eui-react/styles.css"import { builtinTheme } from "@exponential_app/eui"import { globalsCss } from "@exponential_app/eui/web"import { Button, Dialog, DialogContent, DialogTitle, DialogTrigger, Toaster, toast } from "@exponential_app/eui-react/primitives"
const globals = globalsCss(builtinTheme("orange")) // :root + .dark, put it in a <style><Dialog> <DialogTrigger asChild><Button>Open</Button></DialogTrigger> <DialogContent><DialogTitle>Saved views</DialogTitle></DialogContent></Dialog><Button variant="outline" onClick={() => toast.success("Saved")}>Save</Button><Toaster />Compiled in CI
Section titled “Compiled in CI”bun packages/exponential-ui/guides/check.ts react builds guides/react in an empty directory from the packed packages.