Build your own adapter

Bring the opendraw whiteboard to Solid, Angular, Lit or plain JavaScript: render the surface, mount the editor and mirror its state.

Build an adapter for your framework

The Vue, React and Svelte packages are thin layers over @opendrawjs/editor, read packages/react or packages/svelte for complete references, and you can write the same layer for Solid, Angular, Lit or plain JavaScript. The editor does all the work, gestures, keyboard, text editing, rendering, persistence. An adapter only has to do three things:

  1. Render the surface, a focusable host with two stacked canvases.
  2. Mount and dispose, call mountCanvas() when the host exists and dispose() when it goes away.
  3. Mirror state, subscribe to the controller and re-render your UI from its getters.

1. The surface

The host receives pointer events and focus; the canvases are drawn into and never hit-tested. Put data-opendraw-root on the element that wraps your chrome so keyboard shortcuts keep working while focus is on your toolbar.

board.html
<div class="board" data-opendraw-root>
  <div class="board__host" tabindex="0">
    <canvas class="board__static"></canvas>
    <canvas class="board__overlay"></canvas>
  </div>
  <!-- your toolbar, panels and menus go here -->
</div>
board.css
.board__host {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  outline: none;
  touch-action: none;   /* pens and touch draw instead of scrolling */
  user-select: none;
}

.board__static,
.board__overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none; /* the host receives every pointer event */
}

2. Mount (plain JavaScript)

board.js
import { TOOLS, createEditor, mountCanvas, toolLabel } from '@opendrawjs/editor'

const root = document.querySelector('.board')
const editor = createEditor()

const session = mountCanvas(editor, {
  host: root.querySelector('.board__host'),
  staticCanvas: root.querySelector('.board__static'),
  overlayCanvas: root.querySelector('.board__overlay'),
  onContextMenu: ({ x, y, target }) => openMyMenu(x, y, target),
})

// A toolbar from the tool table, kept in sync on every change.
const toolbar = root.querySelector('.toolbar')
for (const def of TOOLS) {
  const button = document.createElement('button')
  button.textContent = toolLabel(def)          // "Rectangle (2)"
  button.onclick = () => editor.setActiveTool(def.tool)
  toolbar.append(button)
}
editor.subscribe((ed) => {
  toolbar.querySelectorAll('button').forEach((button, i) => {
    button.setAttribute('aria-pressed', String(TOOLS[i].tool === ed.activeTool))
  })
})

// Tear down with your component.
// session.dispose()

3. Mirror state (React example)

editor.subscribe() fires on every document or editor-state change, and its getters (activeTool, selectedElements, canUndo, theme, …) are memoized per change. That makes it a natural external store for any reactivity system:

Board.tsx
import { useEffect, useRef, useSyncExternalStore } from 'react'
import { createEditor, mountCanvas, type EditorController } from '@opendrawjs/editor'

/** Mirror the controller into React: re-render on every change. */
export function useEditorState<T>(editor: EditorController, read: (e: EditorController) => T): T {
  return useSyncExternalStore(
    (onChange) => editor.subscribe(onChange),
    () => read(editor),
  )
}

export function Board({ editor }: { editor: EditorController }) {
  const host = useRef<HTMLDivElement>(null)
  const scene = useRef<HTMLCanvasElement>(null)
  const overlay = useRef<HTMLCanvasElement>(null)
  const tool = useEditorState(editor, (e) => e.activeTool)

  useEffect(() => {
    const session = mountCanvas(editor, {
      host: host.current!,
      staticCanvas: scene.current!,
      overlayCanvas: overlay.current!,
    })
    return () => session.dispose()
  }, [editor])

  return (
    <div className="board" data-opendraw-root>
      <div ref={host} className="board__host" tabIndex={0}>
        <canvas ref={scene} className="board__static" />
        <canvas ref={overlay} className="board__overlay" />
      </div>
      <button aria-pressed={tool === 'rectangle'} onClick={() => editor.setActiveTool('rectangle')}>
        Rectangle
      </button>
    </div>
  )
}

// const editor = createEditor() , create once, outside render

Toolbars, menus and icons

  • Build the toolbar from TOOLS (icon name, label, keys) and use toolKeyHint() for the corner key.
  • Build menus from ACTIONS: each entry has a label, icon name and key combo; isActionEnabled() greys items out and runAction() runs them.
  • Shortcuts are already handled by mountCanvas(), your buttons and the keyboard call the same actions.
  • Map every IconName to your icon set once; opendraw never ships icons in the editor.
  • Show the context menu from the onContextMenu callback; the clicked element is already selected.