Elixir implementation of the A2UI protocol — a standard for AI agents to render declarative UI through Phoenix LiveView.
Agents send A2UI v0.9 JSONL messages describing components, and this library renders them as native LiveView function components. User interactions are translated back into A2UI action messages and dispatched to the agent.
Pre-release: This library is under active development. The API may change before 1.0.
Note
This project is developed with significant AI assistance (Claude, Copilot, etc.)
Features
- Protocol parsing — JSONL stream parser for A2UI v0.9 messages (CreateSurface, UpdateComponents, UpdateDataModel, DeleteSurface, Action)
- 18 component types — Text, Button, TextField, CheckBox, ChoicePicker, Slider, DateTimeInput, Image, Icon, Divider, Video, AudioPlayer, Row, Column, List, Card, Tabs, Modal
- Data binding — two-way binding with JSON Pointer paths, resolved at render time
- LiveView macro —
use A2UI.Liveinjects mount hook, message handling, and event dispatch - Agent macro —
use A2UI.Agenthandles connection tracking, process monitoring, and message routing - Transport abstraction —
A2UI.Transportbehaviour with built-in local (process message) transport - CSS classes — BEM-style
a2ui-*classes on all components for easy styling - Demo app —
mix a2ui.demolaunches a restaurant booking agent atlocalhost:4002
Quick Start
defmodule MyAppWeb.AgentLive do use MyAppWeb, :live_view use A2UI.Live def mount(_params, _session, socket) do if connected?(socket) do {:ok, transport} = A2UI.Transport.Local.connect(agent: MyApp.Agent) {:ok, assign(socket, transport: transport)} else {:ok, assign(socket, transport: nil)} end end def render(assigns) do ~H""" <.surface :for={{_id, s} <- @a2ui_surfaces} surface={s} /> """ end @impl A2UI.Live def handle_a2ui_action(action, metadata, socket) do A2UI.Transport.Local.send_action(socket.assigns.transport, action, metadata) {:noreply, socket} end end
On the agent side, use A2UI.Agent handles the GenServer boilerplate:
defmodule MyApp.Agent do use A2UI.Agent @impl A2UI.Agent def handle_connect(conn, state) do A2UI.Agent.send_message(conn, %A2UI.Protocol.Messages.CreateSurface{ surface_id: "main" }) # send UpdateComponents, UpdateDataModel, etc. {:noreply, state} end @impl A2UI.Agent def handle_action(action, _conn, state) do IO.inspect(action.name, label: "action") {:noreply, state} end end
use A2UI.Live gives you:
@a2ui_surfacesassign initialized on mount- Automatic handling of
{:a2ui_message, msg}info messages - Automatic handling of
a2ui_actionanda2ui_input_changeevents - A
handle_a2ui_action/3callback for dispatching actions to your agent
Demo
Run the built-in restaurant booking demo:
Open http://localhost:4002. The demo shows:
- A booking form with text input, date picker, slider, and multi-select
- Two-way data binding — form values update the data model in real time
- Action dispatch — clicking "Reserve Table" sends an action to the agent
- Dynamic UI updates — the agent replaces the form with a confirmation screen
How It Works
A2UI maps naturally to Phoenix LiveView:
| A2UI Concept | Phoenix/LiveView Equivalent |
|---|---|
| Streaming JSONL messages | LiveView WebSocket push to browser |
| Declarative component tree | Phoenix function components + HEEx |
| Data model (JSON document) | LiveView assigns |
| User actions → agent | phx-click / phx-change events |
| Incremental updates | LiveView diff engine |
| Surface lifecycle | LiveView mount/unmount |
Message flow:
- Agent sends A2UI messages (CreateSurface, UpdateComponents, UpdateDataModel)
- Transport delivers them as
{:a2ui_message, msg}to the LiveView process SurfaceManagerapplies messages to update surface state in assigns- Renderer walks the component adjacency list from
"root", dispatching each type to a function component - User interactions trigger
phx-click/phx-changeevents EventHandlerbuilds an A2UI Action message with resolved context bindingshandle_a2ui_action/3callback dispatches the action back to the agent
Component Types
| Category | Components |
|---|---|
| Layout | Row, Column, List |
| Display | Text, Image, Icon, Divider, Video, AudioPlayer |
| Input | TextField, CheckBox, ChoicePicker, Slider, DateTimeInput |
| Container | Card, Tabs, Modal |
| Interactive | Button |
Components use a flat adjacency list — parent-child relationships are ID references, not nesting. This enables incremental streaming and efficient updates by ID.
Custom Components
Override built-in components or add new types by implementing the A2UI.ComponentRenderer behaviour and registering in config.
Override a built-in:
# config/config.exs config :a2ui, component_modules: %{ "Button" => MyApp.A2UI.Button }
Add a new type:
defmodule MyApp.A2UI.StatusBadge do use A2UI.ComponentRenderer @impl true def render(assigns) do status = resolve_prop(assigns.component.props, "status", assigns.ctx, "unknown") assigns = assign(assigns, status: status) ~H""" <span class={"badge badge--#{@status}"}>{@status}</span> """ end end # config/config.exs config :a2ui, component_modules: %{"StatusBadge" => MyApp.A2UI.StatusBadge}
Set use_default_components: false to replace all built-in components with your own. See the A2UI.ComponentRenderer and A2UI.Components.Renderer hexdocs for the full assigns contract, available helpers, and configuration details.
Installation
Add a2ui to your list of dependencies in mix.exs:
def deps do [ {:a2ui, "~> 0.1.0"} ] end
Requires phoenix_live_view ~> 1.0 and phoenix_html ~> 4.0 (pulled in as transitive dependencies).
Static Assets
Copy the JS hooks and CSS to your Phoenix static directory:
cp deps/a2ui/priv/static/a2ui-hooks.js assets/vendor/ cp deps/a2ui/priv/static/a2ui.css assets/vendor/
Then import the hooks in your app.js:
import { A2UIHooks } from "../vendor/a2ui-hooks.js" let liveSocket = new LiveSocket("/live", Socket, { hooks: { ...A2UIHooks } })
And import the CSS in your app.css:
@import "../vendor/a2ui.css";
Development
mix deps.get
mix test
mix compile --warnings-as-errorsRequires Elixir ~> 1.17.
How This Differs
a2ui-elixir is a server-side renderer — the only one in the A2UI ecosystem as of March 2026. All other implementations (Lit, Angular, React, Flutter) parse A2UI JSON on the client and map to native widgets. This library renders on the server, producing HTML that LiveView ships over WebSocket.
- v0.9 spec, 18/18 basic catalog components — full coverage of the current spec
- Server-side binding + function evaluation —
formatString,formatNumber,formatCurrency,formatDate,pluralize, and boolean logic resolved on the server; validation functions (required,regex,email, etc.) run client-side via JS hooks - No client-side A2UI runtime — the browser receives plain HTML and minimal JS hooks
Alternative Implementations
a2ui_lv is another server-side LiveView renderer for A2UI. It takes a similar approach to this library — parsing JSONL messages and rendering Phoenix components — but adds support for both v0.8 and v0.9 of the protocol and includes A2A protocol extension support.
ex_a2ui implements the opposite end of the protocol: it is a protocol server that encodes A2UI surfaces as JSON and pushes them over WebSocket or SSE to client-side renderers. Where this library and a2ui_lv consume A2UI messages, ex_a2ui produces them.
Not Yet Implemented
errormessage (client→server) — v0.9ValidationFailederror feedback to the agentcatalogIdvalidation — verifying components against a remote catalog JSON schema- Remote transports — SSE/HTTP, A2A protocol integration (via a2a-elixir)
Links
License
Apache-2.0 — see LICENSE.