Lightweight Elixir library for Google's A2UI protocol. Serve interactive, agent-driven UI surfaces from any BEAM app via declarative JSON over WebSocket — no Phoenix or LiveView required.
Status: v0.6.0 — complete Builder (all 18 types), debug renderer, 5 demo providers, telemetry, graceful error handling. 232 tests. See ROADMAP.md for the full plan.
A2UI spec: v0.9
A2UI is a protocol where servers produce declarative JSON describing UI, and clients render it natively. The BEAM VM is a natural fit:
- One process per connection — each WebSocket client gets an isolated, lightweight process with its own state. No shared mutable state, millions of concurrent connections.
- Fault tolerance — one client's surface crashes, others are unaffected. The supervisor restarts it.
- Real-time push — broadcasting state changes to connected clients is what the BEAM was built for.
- AI agent UIs — LLMs generate flat JSON component lists, Elixir manages the stateful conversation loop, clients render natively.
| Phoenix LiveView | ex_a2ui | |
|---|---|---|
| Rendering | Server-rendered HTML diffs | Client-rendered native widgets from JSON |
| Clients | Browser only | Browser, Flutter, Angular, React — any A2UI renderer |
| Dependencies | ~12+ packages (full web framework) | ~4 packages (protocol library) |
| AI/LLM friendly | No (HTML templates) | Yes (flat JSON designed for LLM generation) |
| Maturity | Battle-tested, huge ecosystem | Early, A2UI spec is v0.9 |
Use LiveView for full web applications. Use ex_a2ui for:
- AI agent interfaces — an LLM generates interactive UI as structured JSON, not HTML
- Lightweight BEAM app UIs — add a dashboard or config panel to an existing OTP app without pulling in Phoenix
- Cross-platform from one server — same Elixir backend serves browser (Lit), mobile (Flutter), desktop (Angular)
def deps do
[
{:ex_a2ui, "~> 0.6.0"}
]
endDefine a surface provider:
defmodule MyApp.DashboardProvider do
@behaviour A2UI.SurfaceProvider
alias A2UI.Builder, as: UI
@impl true
def init(_opts), do: {:ok, %{count: 0}}
@impl true
def surface(state) do
UI.surface("dashboard")
|> UI.text("count", "Count: #{state.count}")
|> UI.button("inc", "Increment", action: "increment")
|> UI.card("main", children: ["count", "inc"])
|> UI.root("main")
end
@impl true
def handle_action(%A2UI.Action{name: "increment"}, state) do
new_state = %{state | count: state.count + 1}
{:reply, surface(new_state), new_state}
end
def handle_action(_action, state), do: {:noreply, state}
endStart the server:
# In your supervision tree:
children = [
{A2UI.Server, provider: MyApp.DashboardProvider, port: 4000}
]Open http://localhost:4000 — the built-in debug renderer shows your surface and a message log.
git clone https://github.com/23min/ex_a2ui.git
cd ex_a2ui
mix deps.get
mix run demo_server.exs
# Open http://localhost:40005 demo providers are available via query param:
http://localhost:4000/?demo=gallery— Component Gallery (all 18 types, default)http://localhost:4000/?demo=binding— Data Binding (reactive updates)http://localhost:4000/?demo=form— Form Validation (CheckRule)http://localhost:4000/?demo=push— Push Streaming (live metrics)http://localhost:4000/?demo=custom— Custom Components (Catalog)
A2UI (Agent-to-User Interface) is a Google protocol where AI agents generate interactive UI as declarative JSON. Instead of returning text or HTML, an agent describes UI components (buttons, cards, text fields) and the client renders them natively. User interactions flow back to the agent, creating a bidirectional loop.
Elixir app (server) Browser/Mobile (client)
───────────────── ─────────────────────
Build Surface structs ──→ Receive JSON
Encode to A2UI JSON ──→ Render native widgets
←── Send action JSON
Decode, update state ──→ Receive updated surface
Pipe-friendly functions for building surfaces:
alias A2UI.Builder, as: UI
UI.surface("status")
|> UI.text("title", "Hello!")
|> UI.button("go", "Click Me", action: "do_thing")
|> UI.card("container", children: ["title", "go"])Direct protocol types for advanced use cases:
%A2UI.Surface{
id: "status",
components: [
%A2UI.Component{
id: "title",
type: :text,
properties: %{text: %A2UI.BoundValue{literal: "Hello!"}}
}
]
}Both layers produce the same structs. Use whichever fits your style.
| Category | Types |
|---|---|
| Layout | :row, :column, :list |
| Display | :text, :image, :icon, :video, :divider |
| Interactive | :button, :text_field, :checkbox, :date_time_input, :slider, :choice_picker |
| Media | :audio_player |
| Container | :card, :tabs, :modal |
Custom components are supported via Builder.custom/4:
UI.custom(surface, :my_chart, "chart-1", data: bind("/chart/data"))Components bind to a data model using JSON Pointer paths:
# Bind a text component to a data model path
UI.text(surface, "name", bind: "/user/name")
# Set the data model value
|> UI.data("/user/name", "Alice")When the data model updates, bound components update automatically.
lib/
a2ui.ex # Public API, spec version
a2ui/
component.ex # Component struct + 18 standard types
surface.ex # Surface (flat component list + data model)
bound_value.ex # Data binding (literal or path)
action.ex # User interaction events
function_call.ex # Client-evaluated computed values (14 standard functions)
template_child_list.ex # Data-driven children from data arrays
check_rule.ex # Input validation rules
theme.ex # Surface theming
encoder.ex # Elixir structs → A2UI v0.9 JSON wire format
decoder.ex # Incoming action/error JSON → Elixir structs
error.ex # Client error message struct
catalog.ex # Custom component type registry and validation
builder.ex # Pipe-friendly convenience API (all 18 types)
surface_provider.ex # Behaviour: init, surface, handle_action, handle_info, handle_error
socket.ex # WebSock handler (telemetry + graceful error handling)
sse.ex # Server-Sent Events transport adapter (push-only)
endpoint.ex # Plug endpoint (HTTP + WS + SSE routing)
supervisor.ex # OTP Supervisor (Registry + Bandit)
server.ex # OTP supervision tree entry point, push API
priv/
static/
index.html # Built-in debug renderer (all 18 component types)
demo/
component_gallery.ex # All 18 standard types
data_binding.ex # Reactive data binding demo
form_validation.ex # Form with CheckRule validation
push_streaming.ex # Timer-based live metrics
custom_component.ex # Catalog + custom components
Runtime: jason, bandit, websock_adapter
That's it. No Phoenix, no Ecto, no LiveView.
git config core.hooksPath .githooks # one-time setup
mix ci # format + compile + test (232 tests)MIT — see LICENSE.