Installation
Install from npm:
npm install @ludovicm67/simple-whiteboardThen import it once anywhere in your JavaScript. The import has the side effect of registering the <simple-whiteboard> custom element and its tools:
import "@ludovicm67/simple-whiteboard";
Quick start
Drop the element into your markup and add the tool-defaults element to its tools slot to register every built-in tool at once:
<simple-whiteboard locale="en" dotted-background> <simple-whiteboard--tool-defaults slot="tools"> </simple-whiteboard--tool-defaults> </simple-whiteboard>
The element sizes itself to its container, so give it a width and a height (for example a full-height <main>).
Attributes
All attributes are optional and can also be set as properties.
| Attribute | Type | Default | Description |
|---|---|---|---|
locale | string | "en" | The UI language. One of the 24 bundled locales. |
dotted-background | boolean | true | Render a dotted grid behind the content. Use ="false" to hide it. |
hide-locale-picker | boolean | false | Hide the language picker from the menu. |
hide-tool-options | boolean | false | Hide the floating tool-options panel. Handy for a compact or embedded board where it would otherwise cover the canvas. |
debug | boolean | false | Log debug information and show the live pointer coordinates. |
Tools
Tools are individual custom elements placed in the tools slot. Use tool-defaults for the full set, or cherry-pick only the ones you need to keep the toolbar (and your bundle) lean:
<simple-whiteboard> <simple-whiteboard--tool-pointer slot="tools"></simple-whiteboard--tool-pointer> <simple-whiteboard--tool-pen slot="tools"></simple-whiteboard--tool-pen> <simple-whiteboard--tool-eraser slot="tools"></simple-whiteboard--tool-eraser> </simple-whiteboard>
The built-in tool elements:
simple-whiteboard--tool-pointer— select, move and resize itemssimple-whiteboard--tool-move— pan the canvassimple-whiteboard--tool-rect— rectanglesimple-whiteboard--tool-circle— circlesimple-whiteboard--tool-line— linesimple-whiteboard--tool-arrow— arrowsimple-whiteboard--tool-pen— freehand pensimple-whiteboard--tool-text— textsimple-whiteboard--tool-sticky— sticky notesimple-whiteboard--tool-picture— imagesimple-whiteboard--tool-eraser— object erasersimple-whiteboard--tool-clear— clear the canvas
Methods
Grab the element (document.querySelector("simple-whiteboard")) and call these directly.
History
| Method | Returns | Description |
|---|---|---|
undo() | void | Undo the last change. |
redo() | void | Redo the last undone change. |
canUndo() | boolean | Whether there is anything to undo. |
canRedo() | boolean | Whether there is anything to redo. |
Items
| Method | Returns | Description |
|---|---|---|
getItems() | Item[] | The current items, in stacking order. |
getItemById(id) | Item | null | Look up a single item. |
exportItems() | Exported[] | Serialize every item to plain JSON. |
importItems(items) | void | Replace the board with the given exported items. |
importItem(item) | Item | null | Add a single exported item. |
addItem(item, sendEvent?) | void | Append an item. Emits add unless sendEvent is false. |
removeItemById(id, sendEvent?) | void | Remove an item by id. |
partialItemUpdateById(id, updates, sendEvent?) | void | Patch some fields of an item. |
clearWhiteboard() | void | Remove everything and reset the view. |
Stacking order
| Method | Returns | Description |
|---|---|---|
bringItemToFront(id) | void | Move an item on top of everything. |
sendItemToBack(id) | void | Move an item behind everything. |
moveItemForward(id) | void | Move an item one step towards the front. |
moveItemBackward(id) | void | Move an item one step towards the back. |
applyItemsOrder(orderIds) | void | Reorder every item to match a list of ids. |
View & tools
| Method | Returns | Description |
|---|---|---|
setZoom(zoom) | void | Set the zoom level (0.25–4), anchored on the canvas center. |
downloadCurrentCanvasAsPng(options?) | void | Download the current view as a PNG. Options: fileName, backgroundColor. |
setCurrentTool(name) | void | Activate a tool by its name (e.g. "pen"). |
getCurrentTool() | string | The name of the active tool. |
getCanvasElement() | HTMLCanvasElement? | The underlying canvas, if you need it. |
Events
All events are CustomEvents dispatched on the element. Listen with board.addEventListener(name, handler).
| Event | detail | Fired when |
|---|---|---|
ready | { status } | The element is initialized and ready. |
tool-registered | { name } | A tool element has registered itself. |
tool-updated | { type, name } | The active tool changed. |
items-updated | { type, … } | The items changed. See the type values below. |
history-changed | { canUndo, canRedo } | Undo/redo availability changed. |
items-updated types
The detail.type tells you what happened, so you can mirror the change to a store or a remote peer:
| type | Extra detail | Meaning |
|---|---|---|
"add" | item | An item was added. |
"update" | itemId, item | An item was fully replaced. |
"partial-update" | itemId, updates | Some fields of an item changed. |
"remove" | itemId | An item was removed. |
"reorder" | order | The stacking order changed. |
"set" | items | The whole board changed (e.g. undo/redo). |
"clear" | — | The board was cleared. |
Theming
Every surface is driven by CSS custom properties. Override them on the element (or any ancestor) to restyle the whole UI:
simple-whiteboard { --sw-accent: #7c3aed; --sw-board: #fbfaff; --sw-radius: 14px; }
| Property | Default | Role |
|---|---|---|
--sw-board | #fcfcff | Canvas background. |
--sw-surface | #ffffff | Base panel surface. |
--sw-surface-muted | #f2f3f5 | Secondary surface (buttons). |
--sw-accent | #135aa0 | Accent for the active tool, focus rings, sliders. |
--sw-accent-soft | rgba(19,90,160,.12) | Soft accent fill (active/hover). |
--sw-text | #1f2933 | Primary text. |
--sw-text-muted | rgba(31,41,51,.55) | Secondary text. |
--sw-border | rgba(15,23,42,.08) | Panel borders. |
--sw-radius / --sw-radius-sm | 10px / 6px | Corner rounding. |
--sw-shadow | shadow | Floating-panel shadow. |
Framework usage
Because it’s a standard custom element, it works in any framework. Import once at your entry point, then use the tag in your templates.
React
Use the tag directly (React 19+ handles custom elements natively). Set boolean attributes as you would any DOM attribute, and attach a ref to call methods or listen to events.
import "@ludovicm67/simple-whiteboard"; import { useEffect, useRef } from "react"; export function Board() { const ref = useRef(null); useEffect(() => { const el = ref.current; const onChange = (e) => console.log(e.detail); el.addEventListener("items-updated", onChange); return () => el.removeEventListener("items-updated", onChange); }, []); return ( <simple-whiteboard ref={ref} locale="en"> <simple-whiteboard--tool-defaults slot="tools" /> </simple-whiteboard> ); }
Collaboration
The whiteboard doesn’t ship a transport — instead, every change emits an items-updated event, and every method takes an optional sendEvent flag so you can apply a remote change without echoing it back. That’s all you need to build real-time sync over any channel (WebSocket, WebRTC, or, as in the demo, a BroadcastChannel between tabs):
const channel = new BroadcastChannel("whiteboard"); // Broadcast local changes to peers. board.addEventListener("items-updated", (e) => { if (e.detail.type === "add") { channel.postMessage({ type: "add", item: e.detail.item }); } }); // Apply remote changes without re-broadcasting (sendEvent = false). channel.onmessage = (e) => { if (e.data.type === "add") board.importItem(e.data.item); };
See it working: open the live demo in two browser tabs and draw — the boards stay in sync.