tldraw
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.
What this skill does
# 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 keyboardRelated in Web Dev
generating-lwc-components
IncludedLightning Web Components with PICKLES methodology and 165-point scoring. Use this skill when the user creates or edits LWC components, builds wire service patterns, or writes Jest tests for LWC. TRIGGER when: user creates/edits LWC components, touches lwc/**/*.js, .html, .css, .js-meta.xml files, or asks about wire service, SLDS, or Jest LWC tests. DO NOT TRIGGER when: Apex classes (use generating-apex), Aura components, or Visualforce.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Set up queries with useQuery, mutations with useMutation, configure QueryClient caching strategies, implement optimistic updates, and handle infinite scroll with useInfiniteQuery. Use when: setting up data fetching in React projects, migrating from v4 to v5, or fixing object syntax required errors, query callbacks removed issues, cacheTime renamed to gcTime, isPending vs isLoading confusion, keepPreviousData removed problems.
document-processor-api
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
nutrient-document-processing
IncludedProcess documents with Nutrient DWS. Use when the user wants to generate PDFs from HTML or URLs, convert Office/images/PDFs, assemble or split packets, OCR scans, extract text/tables/key-value pairs, redact PII, watermark, sign, fill forms, optimize PDFs, or produce compliance outputs like PDF/A or PDF/UA. Triggers include convert to PDF, merge these PDFs, OCR this scan, extract tables, redact PII, sign this PDF, make this PDF/A, or linearize for web delivery.
tanstack-query
IncludedManage server state in React with TanStack Query v5. Covers useMutationState, simplified optimistic updates, throwOnError, network mode (offline/PWA), and infiniteQueryOptions. Use when setting up data fetching, fixing v4→v5 migration errors (object syntax, gcTime, isPending, keepPreviousData), or debugging SSR/hydration issues with streaming server components.
accelint-nextjs-best-practices
IncludedNext.js performance optimization and best practices. Use when writing Next.js code (App Router or Pages Router); implementing Server Components, Server Actions, or API routes; optimizing RSC serialization, data fetching, or server-side rendering; reviewing Next.js code for performance issues; fixing authentication in Server Actions; or implementing Suspense boundaries, parallel data fetching, or request deduplication.