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:
- Render the surface, a focusable host with two stacked canvases.
- Mount and dispose, call
mountCanvas()when the host exists anddispose()when it goes away. - 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.
<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__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)
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:
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 renderToolbars, menus and icons
- Build the toolbar from
TOOLS(icon name, label, keys) and usetoolKeyHint()for the corner key. - Build menus from
ACTIONS: each entry has a label, icon name and key combo;isActionEnabled()greys items out andrunAction()runs them. - Shortcuts are already handled by
mountCanvas(), your buttons and the keyboard call the same actions. - Map every
IconNameto your icon set once; opendraw never ships icons in the editor. - Show the context menu from the
onContextMenucallback; the clicked element is already selected.