ex_a2ui

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

Why Elixir for A2UI?

A2UI is a protocol where servers produce declarative JSON describing UI, and clients render it natively. The BEAM VM is a natural fit:

When to use this vs Phoenix LiveView

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:

Installation

def deps do
  [
    {:ex_a2ui, "~> 0.6.0"}
  ]
end

Quick start

Define 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}
end

Start 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.

Try the demo

git clone https://github.com/23min/ex_a2ui.git
cd ex_a2ui
mix deps.get
mix run demo_server.exs
# Open http://localhost:4000

5 demo providers are available via query param:

What is A2UI?

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

API layers

Builder (recommended)

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"])

Structs (full control)

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.

Standard components

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"))

Data binding

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.

Project structure

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

Dependencies

Runtime: jason, bandit, websock_adapter

That's it. No Phoenix, no Ecto, no LiveView.

Development

git config core.hooksPath .githooks   # one-time setup
mix ci                                # format + compile + test (232 tests)

License

MIT — see LICENSE.