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:
| Layer | Owns |
|---|---|
| Your application LiveView | routes, markup, item shells, controls, product language, CSS |
| LivePanels | canvas state, item placement, browser protocol, commands, projection |
| Item content LiveViews | the 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
endSecond, 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}
endThat 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.Canvasto mount a canvas; - use
canvas_runtime/1andcanvas_runtime_attrs/2to 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, andLivePanels.Drawingfor 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.