Collaborative Whiteboard Implementation for Your Website
Synchronizing changes during collaborative board editing is one of the toughest challenges in web development. Without the right rendering stack and sync mechanism, your project risks bugs and poor performance. We build online whiteboards for team collaboration: freehand drawing, sticky notes, shapes, lines, and real-time sync. Our engineers implement whiteboards from scratch or integrate existing SDKs (tldraw, Excalidraw) with custom CRDT synchronization. Over 30 projects delivered—from simple note boards to full collaborative editors with WebRTC and audio calls. Teams using our approach save up to 40% development time compared to building in-house.
Choosing the Right Rendering Stack
| Option | Time to MVP | Customizability | Performance (1000 shapes) | Maintenance Cost |
|---|---|---|---|---|
| tldraw | 1–2 days | Medium (SDK) | High (Canvas) | Low (open-source) |
| Konva.js | 3–4 weeks | Full | High (Canvas) | Medium |
| Excalidraw | 1–2 days (fork) | Low (fork is complex) | Medium (SVG) | High (fork) |
| Fabric.js | 2–3 weeks | Medium | Medium (Canvas) | High (aging) |
| SVG + vanilla | 1–2 weeks | Full | Low at >500 shapes | Medium |
For a production product with custom requirements, we prefer tldraw as a base or Konva.js from scratch. Excalidraw forks are possible but the maintenance overhead is high. We select the stack based on your use case and budget.
Why CRDT Is Optimal for Sync
CRDT (Conflict-free Replicated Data Type) automatically resolves edit conflicts without a central server. For whiteboards we use Yjs, a CRDT library optimized for collaborative editing. Yjs is up to 3× faster than OT (Operational Transformation) in benchmarks with 10 concurrent users—a real difference when editing a busy board.
Architecture: Board State
A board is a collection of shape objects. Each shape has a type, geometry, and style.
Common Shape Types
| Type | Properties |
|---|---|
| Rect | x, y, width, height, fill, stroke, strokeWidth, cornerRadius |
| Ellipse | x, y, radiusX, radiusY, fill, stroke |
| Line | points, stroke, strokeWidth |
| Arrow | points, stroke, headStyle |
| Text | content, fontSize, fontFamily, color, width |
| Freehand | points, stroke, strokeWidth, pressure |
| Image | src, x, y, width, height |
CRDT for Shape Sync
Y.Map is perfect for storing each shape by its id:
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
const ydoc = new Y.Doc();
const yshapes = ydoc.getMap<Y.Map<unknown>>('shapes');
const provider = new WebsocketProvider(
process.env.WS_URL!,
`board-${boardId}`,
ydoc
);
// Add a shape
function addShape(shape: Shape) {
const yshape = new Y.Map<unknown>(Object.entries(shape));
yshapes.set(shape.id, yshape);
}
// Update (e.g., on drag)
function updateShape(id: string, patch: Partial<Shape>) {
const yshape = yshapes.get(id);
if (!yshape) return;
ydoc.transact(() => {
Object.entries(patch).forEach(([key, value]) => {
yshape.set(key, value);
});
});
}
// Delete
function deleteShape(id: string) {
yshapes.delete(id);
}
// Reactivity
yshapes.observeDeep((events) => {
events.forEach((event) => {
rerenderCanvas(yshapes);
});
});
Freehand Drawing: Point Optimization
Freehand drawing generates hundreds of points. Sending all of them is expensive. We use the Ramer-Douglas-Peucker algorithm to simplify the curve:
function rdp(points: [number, number][], epsilon: number): [number, number][] {
if (points.length < 3) return points;
let maxDist = 0;
let maxIdx = 0;
const end = points.length - 1;
for (let i = 1; i < end; i++) {
const dist = perpendicularDistance(points[i], points[0], points[end]);
if (dist > maxDist) {
maxDist = dist;
maxIdx = i;
}
}
if (maxDist > epsilon) {
const left = rdp(points.slice(0, maxIdx + 1), epsilon);
const right = rdp(points.slice(maxIdx), epsilon);
return [...left.slice(0, -1), ...right];
}
return [points[0], points[end]];
}
After the user finishes drawing, we run simplification and sync the reduced point set. For smooth curves, we rely on the perfect-freehand library.
Viewport: Pan and Zoom
Our board uses an infinite canvas. All coordinates are in world space; the viewport defines what is visible:
interface Viewport {
x: number; // offset
y: number;
zoom: number; // 0.1 – 4.0
}
// World coordinates → screen
function worldToScreen(wx: number, wy: number, vp: Viewport) {
return {
x: wx * vp.zoom + vp.x,
y: wy * vp.zoom + vp.y,
};
}
// Screen → world (for pointer events)
function screenToWorld(sx: number, sy: number, vp: Viewport) {
return {
x: (sx - vp.x) / vp.zoom,
y: (sy - vp.y) / vp.zoom,
};
}
// Zoom to a point (pinch or scroll wheel)
function zoomAt(vp: Viewport, screenX: number, screenY: number, delta: number): Viewport {
const factor = delta > 0 ? 1.1 : 0.9;
const newZoom = Math.max(0.1, Math.min(4.0, vp.zoom * factor));
const zoomRatio = newZoom / vp.zoom;
return {
x: screenX - (screenX - vp.x) * zoomRatio,
y: screenY - (screenY - vp.y) * zoomRatio,
zoom: newZoom,
};
}
Canvas vs SVG: Choosing Under Load
Below 500 shapes, SVG works perfectly and simplifies hit-testing. Above 1000 shapes with active drawing, we switch to Canvas (2D) or WebGL via Pixi.js. A hybrid approach renders shapes on Canvas and UI elements in HTML on top.
Concrete Case: Ed-Tech Platform
For an ed-tech client, we built a collaborative whiteboard used by teachers and students. We chose tldraw for rapid prototyping, then replaced its sync layer with Yjs to handle up to 30 concurrent users. The result: sync latency dropped from ~200 ms to under 50 ms, and the board remained responsive even with 500+ shapes. The project went from design to production in 10 days.
Undo / Redo Implementation
Yjs provides an UndoManager that groups operations across a 500 ms window:
const undoManager = new Y.UndoManager(yshapes, {
trackedOrigins: new Set([ydoc.clientID]),
captureTimeout: 500,
});
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'z') undoManager.undo();
if (e.ctrlKey && e.key === 'y') undoManager.redo();
});
Board Export to PNG
async function exportToPNG(boardId: string): Promise<Blob> {
const canvas = document.getElementById('whiteboard-canvas') as HTMLCanvasElement;
const shapes = Array.from(yshapes.values()).map(s => shapeFromYMap(s));
const bbox = getBoundingBox(shapes);
const offscreen = new OffscreenCanvas(bbox.width + 80, bbox.height + 80);
const ctx = offscreen.getContext('2d')!;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, offscreen.width, offscreen.height);
renderShapes(ctx, shapes, { x: -bbox.x + 40, y: -bbox.y + 40, zoom: 1 });
return await offscreen.convertToBlob({ type: 'image/png' });
}
Our Work Process
- Analysis: Understand user scenarios, pick the right stack.
- Design: Data architecture, sync protocol, UI layout.
- Implementation: Build canvas engine, integrate Yjs, create custom tools.
- Testing: Unit tests, load testing, sync validation across users.
- Deployment: Docker image, WebSocket setup, CDN for static assets.
Timeline Estimates
- Integrate tldraw with custom Yjs sync: 5–7 days.
- Full whiteboard from scratch (Konva.js + freehand, shapes, viewport, undo, presence, export): 3–4 weeks.
- Add WebRTC for video/audio alongside the board: +1 week.
- All code comes with a one-month free bug-fix warranty.
What's Included
- Architectural documentation (stack choice, data schema, sync protocol).
- Full source code of the board with comments and critical module tests.
- Integration with your authentication system and API.
- Deployment (Docker image, WebSocket config, CDN).
- Team workshop on customization and maintenance.
- One-month free bug-fix guarantee.
- Certified engineers with deep experience in real-time collaboration.
Get a free consultation from our engineers—we'll assess your project and propose the optimal solution. Contact us to discuss the details.







