Skip to content
/ ex_a2ui Public

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. Built on Bandit + WebSock with minimal dependencies.xir

License

Notifications You must be signed in to change notification settings

23min/ex_a2ui

Repository files navigation

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:

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

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:

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

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:

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

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.

About

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. Built on Bandit + WebSock with minimal dependencies.xir

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •