[]previous and next page

Building your own UI

Skip the default interface and drive the canvas from your own toolbar, panels and menus. Everything you need comes from the editor.

Without canvasflow/ui, you still get a complete canvas: rendering, selection handles, snapping guides, text editing, the clipboard, drag and drop, keyboard shortcuts. What's missing is chrome, and the editor gives you everything you need to build your own.

A live CanvasFlow editor runs here when JavaScript is on.

This toolbar is about thirty lines of ShadowJS. Nothing from the default UI is loaded.

The pieces of a toolbar#

Every control in a whiteboard interface comes down to reading some state and calling a method:

Control Read Change
Tool buttons editor.state.toolId editor.setTool(id)
Tool lock editor.state.toolLocked editor.toggleToolLock()
Undo and redo editor.canUndo, editor.canRedo editor.undo(), editor.redo()
Style pickers editor.getSharedStyles() editor.setStyle(key, value)
Opacity editor.getSharedOpacity() editor.setOpacity(value)
Zoom editor.camera.z editor.zoomIn(), zoomOut(), resetZoom(), zoomToFit()
Any command editor.actions.isEnabled(id) editor.actions.run(id)

In plain JavaScript#

toolbar.tsts
import { createCanvasFlow } from 'canvasflow/dom'

const app = createCanvasFlow(document.getElementById('board')!)
const { editor } = app

const toolbar = document.getElementById('toolbar')!
for (const id of ['select', 'draw', 'rectangle', 'arrow', 'text']) {
  const button = document.createElement('button')
  button.textContent = id
  button.onclick = () => editor.setTool(id)
  toolbar.append(button)
}

// Keep the pressed state in sync, however the tool changes (click, keyboard, code).
editor.on('tool', ({ id }) => {
  for (const b of toolbar.querySelectorAll('button')) b.setAttribute('aria-pressed', String(b.textContent === id))
})

In a framework#

Every binding has a headless canvas and a way to follow the editor, so the recipe is the same everywhere: mount the headless canvas, read state with useValue, call editor methods from your buttons. Each framework's page has a toolbar written in its own style:

The pieces below are plain editor calls, so they carry over unchanged. In React:

A style panel follows the same pattern:

ColorPicker.tsxtsx
import { COLOR_TOKENS } from 'canvasflow'
import { useEditor, useValue } from 'canvasflow/react'

export function ColorPicker() {
  const editor = useEditor()
  const color = useValue((e) => e.getSharedStyles().color)
  if (!color) return null // nothing selected supports a color
  return (
    <div role="radiogroup" aria-label="Color">
      {COLOR_TOKENS.map((c) => (
        <button
          key={c}
          role="radio"
          aria-checked={!color.mixed && color.value === c}
          style={{ background: `var(--cf-c-${c})` }}
          onClick={() => editor.setStyle('color', c)}
        />
      ))}
    </div>
  )
}

The --cf-c-* variables come from the canvas theme, so your swatches match the canvas in light and dark.

The editor tells you when a context menu should open, and with what under the pointer:

ts
editor.on('contextmenu', ({ point, shapeId }) => {
  openMyMenu(point, shapeId ? editor.getSelectedIds() : [])
})

Fill it with actions, so labels, shortcuts and enabled states come for free:

ts
const items = ['copy', 'duplicate', 'group', 'bring-to-front', 'delete'].map((id) => ({
  label: editor.actions.get(id)!.label,
  shortcut: editor.actions.shortcutText(id),
  disabled: !editor.actions.isEnabled(id),
  run: () => editor.actions.run(id, 'context-menu'),
}))

Files, export and the clipboard#

The app object handles the browser side:

ts
await app.downloadPng({ scale: 2, background: true })
app.downloadSvg()
await app.copyAsPng()
await app.saveFile() // .canvasflow
await app.openFile()
await app.insertImages() // opens the file picker

Toasts and feedback#

Listen for toast to show the editor's messages in your own style. When undoable is true, offer an Undo button that calls editor.undo().

What you don't have to build#

The DOM host already gives you, without any UI:

  • selection outlines, resize and rotate handles, point handles,
  • snapping guides and the marquee,
  • in-place text editing with IME support,
  • every keyboard shortcut,
  • cursors, including rotated resize cursors,
  • drag and drop of images and files, and paste from anywhere.