LivePanels is a Phoenix LiveView canvas runtime.

It lets your app put LiveView-backed content on a two-dimensional canvas, then move, resize, focus, select, persist, connect, draw on, and share that canvas without making the browser the source of truth.

Use LivePanels when you want application content to behave like workspace items:

  • notes, cards, dashboards, documents, terminals, inspectors, or media panels;
  • graph nodes with typed ports and connectors;
  • vector items, labels, arrows, frames, and annotations;
  • shared boards where multiple participants see the same accepted canvas state;
  • layouts that can be saved, restored, copied, pasted, undone, and redone.

LivePanels does not give you a product shell. It does not choose your navigation, toolbar, visual style, node design, drawing palette, workflow language, or app state. Your Phoenix app owns those.

The split is simple:

LayerOwns
Your application LiveViewroutes, markup, item shells, controls, product language, CSS
LivePanelscanvas state, item placement, browser protocol, commands, projection
Item content LiveViewsthe application UI that lives inside an item frame

The Core Mental Model

A LivePanels application does three things.

First, it declares a canvas and authors items in the canvas markup:

defmodule MyAppWeb.BoardLive do
  use MyAppWeb, :live_view

  use LivePanels.Canvas,
    pubsub: MyApp.PubSub
end

Second, it renders the canvas runtime:

<.canvas_runtime {canvas_runtime_attrs(assigns, class: "board-canvas")}>
  <:item :let={context} id="note-1" at={{80, 80}} size={{560, 360}} resize>
    <section class="note-card">
      <header {drag_handle_attrs(class: "note-card__title")}>
        Note
      </header>

      <div class="note-card__body">
        Put your LiveView or HEEx content here.
      </div>

      <.resize_handles :if={context.view.resizable?} />
    </section>
  </:item>
</.canvas_runtime>

Third, it sends changes through LivePanels helpers instead of mutating canvas assigns directly:

<button {add_item_attrs(:note)}>New note</button>
def handle_event("archive-note", %{"id" => id}, socket) do
  socket =
    socket
    |> LivePanels.Canvas.Item.merge_props!(id, %{archived: true})
    |> LivePanels.Canvas.Item.hide!(id)

  {:noreply, socket}
end

That is enough structure for LivePanels to keep browser gestures, server commands, persistence, shared canvases, graph connectors, and vector items on the same path.

What The Browser Does

The browser makes pointer work feel immediate.

During a drag, resize, graph connection, point edit, or drawing gesture, the browser can preview the interaction locally. When the gesture becomes a real change, the browser sends a LivePanels command payload to the LiveView process.

That keeps the UI responsive without asking the browser to own accepted canvas state.

What The Server Does

The server owns accepted canvas state.

When a command arrives, LivePanels can validate the item, check capabilities, run application policy, update state, persist layout when configured, and project the new render data back to the browser.

This is why the same item can be moved by a drag handle, a toolbar button, a server event, a graph layout helper, or a shared participant without creating five different mutation paths.

What Your App Does

The application owns the visible app.

You decide what an item looks like:

<section class={"card card-#{LivePanels.Item.prop(context.item, :tone, "neutral")}"}>
  <header {drag_handle_attrs()}>
    {context.item.label}
  </header>

  {context.content}
</section>

You decide where controls live:

<nav class="toolbar">
  <button {add_item_attrs(:note)}>Note</button>
  <button {layout_attrs(:grid)}>Layout</button>
  <button {undo_attrs()}>Undo</button>
</nav>

You decide what a graph connector means in your product. LivePanels only needs to know that there is a connection between two declared endpoints. You decide what a vector item means. LivePanels only needs to know that it is a committed canvas item with vector state.

The Rule To Remember

Use public LivePanels helpers for behavior. Use your own markup and CSS for presentation.

That means:

  • use LivePanels.Canvas to mount a canvas;
  • use canvas_runtime/1 and canvas_runtime_attrs/2 to render it;
  • use toolkit attrs such as drag_handle_attrs/1, resize_handles/1, add_item_attrs/2, graph attrs, and drawing attrs for browser commands;
  • use owner modules such as LivePanels.Canvas.Item, LivePanels.Graph, and LivePanels.Drawing for server commands;
  • avoid reaching into private runtime assigns, browser controllers, or compositor modules.

The rest of the guides show those pieces one at a time.