Render A2UI in Solid
Install
Section titled “Install”Two packages: @exponential_app/eui (the catalog, themes, reducer and host runtime) and @exponential_app/eui-solid (the renderer). It needs Solid 1.9 with vite-plugin-solid. The surface injects its own styles, so a stylesheet setup is unnecessary.
npm install @exponential_app/eui @exponential_app/eui-solid solid-jsThe guide project’s whole dependency list and its Vite config:
{ "name": "exponential-ui-guide-solid", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build" }, "dependencies": { "@exponential_app/eui": "^0.12.0", "@exponential_app/eui-solid": "^0.12.0", "solid-js": "^1.9.0" }, "devDependencies": { "typescript": "^5.7.2", "vite": "^7.1.0", "vite-plugin-solid": "^2.11.0" }}import { defineConfig } from "vite"import solid from "vite-plugin-solid"
// Solid's JSX needs its compiler.export default defineConfig({ plugins: [solid()] })Paint a surface
Section titled “Paint a surface”createSurface reduces A2UI messages into a tree and a data model (a Solid store); <ExponentialSurface> paints it in a theme and a mode. The plugin object takes icons, onAction, onInput, openUrl and extra functions.
import { onCleanup, onMount } from "solid-js"import { CORE_CATALOG_ID } from "@exponential_app/eui"import { ExponentialSurface, createSurface } from "@exponential_app/eui-solid"import type { A2uiMessage } from "@exponential_app/eui-solid"import { Button } from "@exponential_app/eui-solid/primitives"import "@exponential_app/eui-solid/styles.css"
export function App() { // The surface state: A2UI v0.9 server messages in, the tree and data out. const surface = createSurface({ surfaceId: `main` })
// Messages from the agent, here one JSON message per line over SSE. onMount(() => { surface.apply({ createSurface: { surfaceId: `main`, catalogId: CORE_CATALOG_ID } }) surface.apply({ updateComponents: { surfaceId: `main`, components: [ { id: `root`, component: `Column`, children: [`title`, `body`] }, { id: `title`, component: `Text`, text: `Hello from the agent`, variant: `title` }, { id: `body`, component: `Text`, text: { path: `/status` } }, ], }, }) surface.apply({ updateDataModel: { surfaceId: `main`, path: `/status`, value: `Waiting for the agent…` } }) const events = new EventSource(`/a2ui`) events.onmessage = (e) => surface.apply(JSON.parse(e.data) as A2uiMessage) onCleanup(() => events.close()) })
// Paints it; the theme is data, the mode a prop. Actions go to your agent. // Below it, a primitive from the same set (the stylesheet above styles it). return ( <> <ExponentialSurface surface={surface} theme="orange" mode="dark" host={{ onAction: (event) => void fetch(`/action`, { method: `POST`, body: JSON.stringify(event) }) }} /> <Button variant="outline" onClick={() => surface.apply({ updateDataModel: { surfaceId: `main`, path: `/status`, value: `Asked again` } })}> Ask again </Button> </> )}The DOM, the compiled stylesheet and the themes are the React renderer’s, checked case by case in headless Chromium (DOM, computed styles, frames and pixels).
With a host
Section titled “With a host”One ExponentialHost per app reads messages from a transport and posts actions back; <HostSurface> paints one of its surfaces by id, with a fallback until the agent has created it.
const host = new ExponentialHost({ transport: new JsonlStreamTransport({ url: "/a2ui.jsonl", postUrl: "/action" }) })onMount(() => host.connect()); onCleanup(() => host.close())return <HostSurface host={host} surfaceId="main" theme="orange" fallback={<p>Waiting for the agent…</p>} />Extensions use defineSolidExtension; the hooks have Solid names (createSurface, createHostSurface, createField). More in the Solid renderer’s README.
// An extension = a catalog (JSON: what the model may use) + one painter per// NATIVE component. `Metric` is a macro: it expands into Card + Text +// TrendLine in the reducer and needs no painter on any platform.import { defineExtension } from "@exponential_app/eui"import type { ExtensionDef } from "@exponential_app/eui"import { defineSolidExtension } from "@exponential_app/eui-solid"import type { ExtensionComponentProps } from "@exponential_app/eui-solid"import catalog from "./trendline.extension.json"
// JSON imports widen literals, hence the cast; defineExtension checks it and throws listing every problem.export const metricsCatalog = defineExtension(catalog as unknown as ExtensionDef)
// Solid props are live getters: read them inside JSX/functions, never destructure.function TrendLine(props: ExtensionComponentProps) { const values = () => (Array.isArray(props.props.values) ? props.props.values.map(Number) : []) const height = () => Number(props.props.height ?? 48) const points = () => { const v = values() const min = Math.min(...v), max = Math.max(...v) const y = (n: number) => (max === min ? 50 : 100 - ((n - min) / (max - min)) * 100) return v.map((n, i) => `${(i / Math.max(v.length - 1, 1)) * 100},${y(n)}`).join(` `) } return ( <div {...props.rootProps} style={{ height: `${height()}px`, width: `100%` }}> <svg viewBox="0 0 100 100" preserveAspectRatio="none" width="100%" height={height()} role="img" aria-label={`${values().length} values`}> <polyline points={points()} fill="none" stroke={props.theme.modes[props.mode].color.primary} stroke-width="2" vector-effect="non-scaling-stroke" /> </svg> </div> )}
// Per surface: <ExponentialSurface … extensions={[metrics]} /> and new ExponentialHost({extensions: [metricsCatalog]})// (or registerExtension({catalog, components}) once for every surface on the page).export const metrics = defineSolidExtension({ catalog: metricsCatalog, components: { TrendLine } })A surface in TypeScript
Section titled “A surface in TypeScript”defineSurface from @exponential_app/eui works unchanged in a Solid project: the typed tree becomes the same messages an agent sends, and createSurface applies them. The file compiles under this project’s tsconfig and runs under Node, with nothing Solid-specific in it. The DSL is the one from Write a surface in TypeScript; the second file paints it with the Solid renderer.
// The typed DSL from a Solid project: the same `defineSurface` as the// TypeScript guide, compiled by this project's tsconfig (bundler resolution,// Solid's JSX settings) with nothing changed. Every component's props// autocomplete from the catalog; a wrong prop is a type error, a wrong value// a validator issue. `node src/defined.ts` prints the JSONL a host consumes.import { bind, call, defineSurface, event, run, ui } from "@exponential_app/eui"
const entries = ui.Stack({ gap: `md` }, [ ui.Section({ title: `Entries`, icon: `ui-clock`, count: 2 }), ui.List( { divided: true }, { template: ui.Row({ title: bind(`label`), subtitle: bind(`detail`), icon: `ui-clock`, chevron: `right`, pressable: true }, undefined, { on: { press: event(`openEntry`, { id: bind(`id`) }) } }), path: `/entries/rows`, key: `id` } ), ui.EmptyState({ icon: `ui-clock`, title: `No hours yet`, description: `Log the hours you worked and they appear here.`, actionLabel: `Log hours` }, undefined, { visible: call(`eq`, { a: 0, b: bind(`/entries/count`) }), on: { press: run(`app.toast`, { title: `Soon` }) }, }),])
export const defined = defineSurface(entries, { surfaceId: `working-hours`, data: { "/entries": { count: 2, rows: [{ id: `e1`, label: `support, admin, orgs`, detail: `2026-10-23 · 3 h` }, { id: `e2`, label: `review, onboarding`, detail: `2026-10-22 · 4.5 h` }] } },})
if (defined.issues.length) throw new Error(defined.issues.map((i) => `${i.id}: ${i.message}`).join(`\n`))console.log(defined.jsonl)import { ExponentialSurface, createSurface } from "@exponential_app/eui-solid"import type { A2uiMessage } from "@exponential_app/eui-solid"import { defined } from "./defined"
// A surface written in TypeScript, painted by the Solid renderer: the// messages defineSurface produced are the ones an agent would send.export function Defined() { const surface = createSurface({ surfaceId: defined.surfaceId }) for (const message of defined.messages) surface.apply(message as A2uiMessage) return <ExponentialSurface surface={surface} theme="orange" mode="dark" host={{ onAction: (event) => console.log(event) }} />}Primitives
Section titled “Primitives”@exponential_app/eui-solid/primitives is the React primitive set on Solid, module for module, with the same DOM and classes (one set of recipes, checked against React in headless Chromium). @exponential_app/eui-solid/styles.css is the same stylesheet as React’s; globalsCss(theme) from @exponential_app/eui/web themes them outside a surface. The names differ in three ways: class for className, as for asChild, and create* for the use* hooks.
import "@exponential_app/eui-solid/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-solid/primitives"
const globals = globalsCss(builtinTheme("orange")) // :root + .dark, put it in a <style><Dialog> <DialogTrigger as={Button}>Open</DialogTrigger> <DialogContent><DialogTitle>Saved views</DialogTitle></DialogContent></Dialog><Button variant="outline" class="ml-2" onClick={() => toast.success("Saved")}>Save</Button><Toaster />Compiled in CI
Section titled “Compiled in CI”bun packages/exponential-ui/guides/check.ts solid builds guides/solid in an empty directory from the packed packages.