tldraw
$
npx mdskill add TerminalSkills/skills/tldrawEmbeds collaborative infinite canvas experiences in React apps using tldraw
- Enables developers to add whiteboards, diagram editors, and visual tools with collaboration
- Leverages tldraw's shape system, camera controls, and multiplayer support
- Uses React components and TypeScript for customization and integration
- Delivers interactive canvas experiences via embedded SDK in web applications
SKILL.md
.github/skills/tldrawView on GitHub ↗
---
name: tldraw
description: Expert guidance for tldraw, the open-source library for creating infinite canvas experiences in React applications. Helps developers embed collaborative whiteboards, diagram editors, and visual tools with tldraw's shape system, camera controls, and multiplayer support.
license: Apache-2.0
compatibility: No special requirements
metadata:
author: terminal-skills
version: 1.0.0
category: development
tags:
- canvas
- whiteboard
- drawing
- collaboration
- react
---
# tldraw — Infinite Canvas SDK
## Overview
Tldraw, the open-source library for creating infinite canvas experiences in React applications. Helps developers embed collaborative whiteboards, diagram editors, and visual tools with tldraw's shape system, camera controls, and multiplayer support.
## Instructions
### Basic Setup
Embed a full-featured whiteboard in a React app:
```tsx
// src/components/Whiteboard.tsx — Basic tldraw canvas
import { Tldraw } from "tldraw";
import "tldraw/tldraw.css";
export function Whiteboard() {
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw
// Optional: persist to a backend
persistenceKey="my-whiteboard"
// Optional: customize available tools
// tools={[...defaultTools, MyCustomTool]}
/>
</div>
);
}
```
### Custom Shapes
Define domain-specific shapes for your application:
```tsx
// src/shapes/TaskCardShape.tsx — Custom shape for a project management board
import {
BaseBoxShapeUtil,
TLBaseShape,
HTMLContainer,
T,
DefaultColorStyle,
ShapeUtil,
} from "tldraw";
// Define the shape's data structure
type TaskCard = TLBaseShape<
"task-card",
{
title: string;
description: string;
assignee: string;
priority: "low" | "medium" | "high" | "critical";
status: "todo" | "in-progress" | "review" | "done";
w: number;
h: number;
}
>;
// Shape utility — tells tldraw how to render and interact with the shape
export class TaskCardShapeUtil extends BaseBoxShapeUtil<TaskCard> {
static override type = "task-card" as const;
// Default values when a new task card is created
getDefaultProps(): TaskCard["props"] {
return {
title: "New Task",
description: "",
assignee: "Unassigned",
priority: "medium",
status: "todo",
w: 280, // Default width in pixels
h: 160, // Default height
};
}
// Render the shape as HTML (supports full React components)
component(shape: TaskCard) {
const priorityColors = {
low: "#4ade80",
medium: "#facc15",
high: "#fb923c",
critical: "#ef4444",
};
return (
<HTMLContainer
id={shape.id}
style={{
padding: "12px",
borderRadius: "8px",
border: `2px solid ${priorityColors[shape.props.priority]}`,
backgroundColor: "white",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
display: "flex",
flexDirection: "column",
gap: "8px",
pointerEvents: "all", // Enable click/drag on the HTML content
}}
>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<strong style={{ fontSize: "14px" }}>{shape.props.title}</strong>
<span style={{
fontSize: "10px",
padding: "2px 6px",
borderRadius: "4px",
backgroundColor: priorityColors[shape.props.priority],
color: "white",
}}>
{shape.props.priority}
</span>
</div>
<p style={{ fontSize: "12px", color: "#666", margin: 0 }}>
{shape.props.description || "No description"}
</p>
<div style={{ fontSize: "11px", color: "#999", marginTop: "auto" }}>
👤 {shape.props.assignee} • {shape.props.status}
</div>
</HTMLContainer>
);
}
// Render a simplified version when zoomed out (performance optimization)
indicator(shape: TaskCard) {
return <rect width={shape.props.w} height={shape.props.h} rx={8} />;
}
}
```
### Accessing the Editor API
Programmatically control the canvas:
```typescript
// src/hooks/useCanvasActions.ts — Programmatic canvas manipulation
import { useEditor } from "tldraw";
export function useCanvasActions() {
const editor = useEditor();
// Create shapes programmatically
function addTaskCard(title: string, x: number, y: number) {
editor.createShape({
type: "task-card",
x,
y,
props: {
title,
description: "",
assignee: "Unassigned",
priority: "medium",
status: "todo",
w: 280,
h: 160,
},
});
}
// Export canvas as image
async function exportAsImage() {
const shapeIds = editor.getCurrentPageShapeIds();
if (shapeIds.size === 0) return;
const blob = await editor.getSvg([...shapeIds], {
scale: 2, // 2x resolution
background: true, // Include background color
});
return blob;
}
// Zoom to fit all content
function zoomToFit() {
editor.zoomToFit({ animation: { duration: 300 } });
}
// Get all shapes of a specific type
function getTaskCards() {
return editor
.getCurrentPageShapes()
.filter((s) => s.type === "task-card") as TaskCard[];
}
// Update a shape's properties
function updateTaskStatus(shapeId: string, status: string) {
editor.updateShape({
id: shapeId,
type: "task-card",
props: { status },
});
}
// Listen to selection changes
function onSelectionChange(callback: (shapes: TLShape[]) => void) {
return editor.store.listen(
() => {
const selectedIds = editor.getSelectedShapeIds();
const shapes = selectedIds.map((id) => editor.getShape(id)!);
callback(shapes);
},
{ source: "user", scope: "session" }
);
}
return {
addTaskCard,
exportAsImage,
zoomToFit,
getTaskCards,
updateTaskStatus,
onSelectionChange,
};
}
```
### Multiplayer with Yjs
Add real-time collaboration using Yjs:
```tsx
// src/components/MultiplayerWhiteboard.tsx — Collaborative canvas with Yjs sync
import { Tldraw, useEditor } from "tldraw";
import { useYjsStore } from "@tldraw/yjs";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
export function MultiplayerWhiteboard({ roomId }: { roomId: string }) {
const doc = useMemo(() => new Y.Doc(), []);
const provider = useMemo(
() => new WebsocketProvider("wss://yjs.example.com", roomId, doc),
[doc, roomId]
);
// useYjsStore bridges tldraw's store with Yjs for real-time sync
const store = useYjsStore({
yDoc: doc,
provider,
roomId,
});
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw
store={store}
// Users see each other's cursors and selections automatically
/>
</div>
);
}
```
### Snapshot and Restore
Save and load canvas state:
```typescript
// src/persistence/snapshots.ts — Save/load canvas state
import { Editor, TLStoreSnapshot } from "tldraw";
// Save the current canvas state as a JSON snapshot
function saveSnapshot(editor: Editor): TLStoreSnapshot {
return editor.store.getSnapshot();
}
// Restore canvas from a saved snapshot
function loadSnapshot(editor: Editor, snapshot: TLStoreSnapshot) {
editor.store.loadSnapshot(snapshot);
}
// Save to your backend
async function persistToServer(editor: Editor, documentId: string) {
const snapshot = saveSnapshot(editor);
await fetch(`/api/documents/${documentId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(snapshot),
});
}
// Auto-save on changes (debounced)
function setupAutoSave(editor: Editor, documentId: string) {
let timeout: NodeJS.Timeout;
editor.store.listen(() => {
clearTimeout(timeout);
timeout = setTimeout(() => persistToServer(editor, documentId), 2000);
}, { source: "user", scope: "document" });
}
```
## Installation
```bash
npm install tldraw
# For multiplayer
npm install @tldraw/yjs yjs y-websocket
```
## Examples
### Example 1: Setting up Tldraw with a custom configuration
**User request:**
```
I just installed Tldraw. Help me configure it for my TypeScript + React workflow with my preferred keybindings.
```
The agent creates the configuration file with TypeScript-aware settings, configures relevant plugins/extensions for React development, sets up keyboard shortcuts matching the user's preferences, and verifies the setup works correctly.
### Example 2: Extending Tldraw with custom functionality
**User request:**
```
I want to add a custom custom shapes to Tldraw. How do I build one?
```
The agent scaffolds the extension/plugin project, implements the core functionality following Tldraw's API patterns, adds configuration options, and provides testing instructions to verify it works end-to-end.
## Guidelines
1. **Full viewport container** — tldraw needs a container with explicit dimensions; `position: fixed; inset: 0` is the simplest approach
2. **Custom shapes for domain logic** — Don't force everything into draw/text; create shapes that represent your domain (tasks, nodes, cards)
3. **Use HTMLContainer for complex UI** — Custom shapes can render full React components, not just SVG
4. **Indicator for zoom performance** — Always implement `indicator()` — it renders when zoomed out instead of the full component
5. **Persist with snapshots** — Use `store.getSnapshot()` for server persistence; use Yjs for real-time sync
6. **Debounce auto-save** — Canvas changes fire rapidly during drawing; save every 2-3 seconds, not on every change
7. **Export as SVG, not PNG** — SVG exports are resolution-independent and much smaller in file size
8. **Test on touch devices** — tldraw supports touch natively, but custom shapes may need pointer event adjustments
More from TerminalSkills/skills