Create Your First Canvas

Copy Markdown View Source

This guide builds a note board in a Phoenix LiveView app.

It covers the first-use path only:

  • install the Elixir package and browser runtime;
  • define one item-content LiveView;
  • author one canvas item in HEEx;
  • render the canvas runtime;
  • add drag, resize, add, focus, and remove behavior.

Graph connectors, drawing tools, shared canvases, persistence, history, and custom render loops come later.

Install LivePanels

Add the package:

# mix.exs
{:livepanels, "~> 0.1.0"}

Fetch dependencies:

mix deps.get

Install the browser package into your Phoenix assets:

mix livepanels.install
cd assets && npm install

Import the browser hooks and runtime CSS beside your existing LiveView setup:

import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import { LivePanelsHooks } from "livepanels";
import "livepanels/runtime.css";

const hooks = {
  ...LivePanelsHooks
};

const liveSocket = new LiveSocket("/live", Socket, { hooks });

If your Phoenix app imports CSS from assets/css/app.css instead of from JavaScript, put the CSS import there:

@import "livepanels/runtime.css";

Generate Or Write The First Canvas

The fastest route is the generator:

mix livepanels.new canvas --yes

That creates a working route LiveView at lib/<app>_web/live/canvas_live.ex. It is a good first file to inspect because it contains the same pieces this guide explains manually.

If you are wiring by hand, start with a normal Phoenix route:

scope "/", MyAppWeb do
  pipe_through :browser

  live "/board", BoardLive
end

Define The Canvas LiveView

The canvas LiveView is an ordinary LiveView with LivePanels.Canvas attached. Declare initial panels directly inside canvas_runtime/1:

defmodule MyAppWeb.BoardLive do
  use MyAppWeb, :live_view

  use LivePanels.Canvas,
    pubsub: MyApp.PubSub

  @impl true
  def mount(_params, _session, socket) do
    {:ok, socket}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <main class="board-page">
      <.canvas_runtime {canvas_runtime_attrs(assigns, class: "board-canvas")}>
        <:item :let={context} id="welcome-note" at={{80, 80}} size={{640, 420}} resize>
          <section class={"board-item #{if context.selected?, do: "is-selected"}"}>
            <header {drag_handle_attrs(class: "board-item__title")}>
              <span>Welcome note</span>
            </header>

            <div class="board-item__body">
              <article class="note-content">
                <p>This content lives directly inside the item declaration.</p>
              </article>
            </div>

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

The important parts are:

  • use LivePanels.Canvas attaches the canvas runtime to the LiveView;
  • <:item> declares an item from markup;
  • id, at, and size declare the item identity and initial frame;
  • canvas_runtime_attrs(assigns, ...) passes runtime assigns to the component;
  • canvas_runtime/1 renders the canvas root, layers, and item loop;
  • drag_handle_attrs/1 and resize_handles/1 connect your markup to the runtime protocol.

Add Enough CSS To See It

LivePanels ships structural runtime CSS, not your app's item design. Add application styles that fit your app. A starter board can begin with:

.board-page {
  block-size: 100vh;
  display: grid;
  grid-template-rows: auto 1fr;
}

.board-toolbar {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem;
  border-bottom: 1px solid #d7dde7;
}

.board-canvas {
  min-block-size: 0;
  background: #f7f9fc;
}

.board-item {
  height: 100%;
  overflow: hidden;
  border: 1px solid #c7d2e0;
  border-radius: 6px;
  background: white;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}

.board-item__title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid #e2e8f0;
  cursor: grab;
}

.board-item__body {
  padding: 0.75rem;
}

Try The First Interactions

Start the Phoenix server and visit the route:

mix phx.server

You should be able to:

  • click New note to add an item;
  • drag the titlebar to move the item;
  • resize from the handles;
  • click the item to focus it;
  • remove it with the app-rendered close button.

If those interactions do not work, check these in order:

  • the LivePanels browser hooks are included in LiveSocket;
  • runtime.css is imported;
  • the page renders canvas_runtime_attrs(assigns, ...);
  • controls use LivePanels attrs instead of hand-written phx-click payloads;
  • mix livepanels.check passes.

Use Find The API You Need when you know the thing you want to do and need the right module. Use Add Items when you want to understand item declarations and placement. Use Render Item Content With LiveViews when item content needs to communicate with the canvas.