@devkalmon/canvu
v0.3.5
Published
Vector-first infinite canvas (SVG) with pan, zoom, React bindings, and optional plugins
Readme
canvu
canvu is a vector-first SVG canvas for the browser with pan, zoom, tools, persistence, plugins, and realtime collaboration.
Install
npm install canvu react react-dom lucide-reactRequired peers:
reactreact-domlucide-react
Optional peers:
@ai-sdk/reactai
Entry points
| Import | Purpose |
| --- | --- |
| canvu | Core primitives: camera, scene, SVG renderer, input helpers, shape builders |
| canvu/react | React runtime: VectorViewport, VectorToolbar, persistence, plugin system |
| canvu/plugins/chatbot | Plug and play chatbot plugin |
| canvu/plugins/realtime | Plug and play realtime collaboration plugin + advanced hooks |
| canvu/plugins/tldraw | tldraw import helpers |
Quick start
import { useState } from "react";
import {
useVectorCanvasDocument,
VectorCanvas,
VectorToolbar,
VectorViewport,
} from "canvu/react";
export function Board() {
const [toolId, setToolId] = useState("hand");
const [toolLocked, setToolLocked] = useState(false);
const doc = useVectorCanvasDocument({ persistenceKey: "board.v1" });
if (!doc.isHydrated) return null;
return (
<VectorCanvas.Root style={{ height: "100dvh", width: "100%" }}>
<VectorCanvas.Body>
<VectorCanvas.Main>
<VectorCanvas.ViewportSurface>
<VectorViewport
ariaLabel="Board"
toolId={toolId}
toolLocked={toolLocked}
onToolChangeRequest={setToolId}
items={doc.items}
onItemsChange={doc.onItemsChange}
interactive
plugins={[]}
toolbar={
<VectorCanvas.Toolbar>
<VectorToolbar
value={toolId}
onChange={setToolId}
toolLocked={toolLocked}
onToolLockedChange={setToolLocked}
aria-label="Canvas tools"
/>
</VectorCanvas.Toolbar>
}
/>
</VectorCanvas.ViewportSurface>
</VectorCanvas.Main>
</VectorCanvas.Body>
</VectorCanvas.Root>
);
}Plugin-first architecture
The recommended React DX is plugin-first:
import { chatbotPlugin } from "canvu/plugins/chatbot";
import { realtimeCollaborationPlugin } from "canvu/plugins/realtime";
<VectorViewport
ariaLabel="Board"
items={doc.items}
onItemsChange={doc.onItemsChange}
toolId={toolId}
onToolChangeRequest={setToolId}
interactive
plugins={[
chatbotPlugin({ chatApi: "/api/chat" }),
realtimeCollaborationPlugin({
url,
roomId,
peer: {
id: peerId,
displayName: "Kalmon",
color: "#7c3aed",
image: avatarUrl,
},
}),
]}
/>No CanvasPluginHost is required. The plugin runtime lives inside VectorViewport.
Custom tools
Use createToolPlugin(...) for isolated tools.
import { createToolPlugin } from "canvu/react";
import { Pin } from "lucide-react";
const reviewPinPlugin = createToolPlugin({
id: "review-pin",
label: "Review",
shortcutHint: "R",
icon: <Pin aria-hidden />,
createItem: ({ id, bounds }) => createReviewPinItem(id, bounds),
});
<VectorViewport plugins={[reviewPinPlugin]} ... />If the tool belongs to a larger feature, keep it inside that feature plugin.
Advanced hooks
Low-level hooks remain public for advanced customization:
useRealtimeSession(...)useRealtimeComments(...)useCanvuPluginContext()useCanvuViewportContext()useCanvuDocumentContext()useCanvuPluginContribution(...)createCanvuPlugin(...)
Use them for custom plugins and bespoke UIs, not for the common path.
CSS requirement
The interactive surface must use touch-action: none so the browser does not steal gestures.
Security
childrenSvg is injected via innerHTML.
Only pass trusted or sanitized SVG.
