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.
Installable skillApache-2.0By TerminalSkillsUpdated Oct 30, 2026
INTENDED FORClaude CodeCodexCursorGeminiInstall in a listed agent.
instructions.md
---
name: tldraw-efeb019b4300
description: >-
tldraw is a React SDK for building infinite-canvas apps — whiteboards, diagram
editors, and visual tools — with a shape system, camera controls, snapshots,
and real-time multiplayer. Use when embedding a collaborative canvas or
whiteboard in a React app, defining custom shapes, driving the editor
programmatically, exporting the canvas to an image, or adding multiplayer
sync. Note: production use requires a tldraw license key.
license: Apache-2.0
compatibility: "tldraw SDK 5 with React 18.2+ or 19.2.1+. Production use requires a tldraw license key."
metadata:
author: terminal-skills
version: "1.1.0"
category: development
tags: "canvas, whiteboard, drawing, collaboration, react"
repository: https://github.com/tldraw/tldraw
---
# tldraw — Infinite Canvas SDK
## Overview
tldraw is a React SDK for building infinite-canvas experiences: whiteboards,
diagram editors, and visual tools. You drop in the `<Tldraw>` component, extend
it with custom shapes that render as React/HTML or SVG, drive the canvas through
the `Editor` API, persist state with snapshots, and add real-time collaboration
with `@tldraw/sync`.
## Instructions
### Basic Setup
Install and embed a full-featured canvas. The container needs explicit
dimensions, and you must import the stylesheet.
```tsx
// src/components/Whiteboard.tsx
import { Tldraw } from "tldraw";
import "tldraw/tldraw.css";
export function Whiteboard() {
return (
<div style={{ position: "fixed", inset: 0 }}>
{/* persistenceKey saves to the browser's IndexedDB and syncs across tabs */}
<Tldraw persistenceKey="design-review-board" />
</div>
);
}
```
### Custom Shapes
Model your domain with a shape util. `BaseBoxShapeUtil` gives you a resizable
box; `component()` renders it (full React is allowed), `getIndicatorPath()`
returns the selection outline as a `Path2D` (it replaced the JSX `indicator()`
in SDK 5).
```tsx
// src/shapes/TaskCardShapeUtil.tsx
import { BaseBoxShapeUtil, HTMLContainer, T, TLShape } from "tldraw";
// Register the props under the shape's type name (a global registry: keep the name unique).
declare module "tldraw" {
export interface TLGlobalShapePropsMap {
"task-card": { title: string; assignee: string; priority: "low" | "high"; w: number; h: number };
}
}
type TaskCard = TLShape<"task-card">;
export class TaskCardShapeUtil extends BaseBoxShapeUtil<TaskCard> {
static override type = "task-card" as const;
static override props = { // validated by the store
title: T.string, assignee: T.string, priority: T.literalEnum("low", "high"), w: T.number, h: T.number,
};
getDefaultProps(): TaskCard["props"] {
return { title: "New task", assignee: "Unassigned", priority: "low", w: 280, h: 140 };
}
component(shape: TaskCard) {
const border = shape.props.priority === "high" ? "#ef4444" : "#4ade80";
return (
<HTMLContainer
style={{
padding: 12,
border: `2px solid ${border}`,
borderRadius: 8,
background: "white",
pointerEvents: "all", // let the HTML receive clicks
}}
>
<strong>{shape.props.title}</strong>
<div style={{ fontSize: 11, color: "#666" }}>{shape.props.assignee}</div>
</HTMLContainer>
);
}
getIndicatorPath(shape: TaskCard) {
const path = new Path2D();
path.roundRect(0, 0, shape.props.w, shape.props.h, 8);
return path;
}
}
```
Register it on the component:
```tsx
import { Tldraw } from "tldraw";
import { TaskCardShapeUtil } from "./shapes/TaskCardShapeUtil";
<Tldraw shapeUtils={[TaskCardShapeUtil]} persistenceKey="sprint-board" />;
```
### Driving the Editor API
Grab the `Editor` with the `useEditor()` hook (inside the `<Tldraw>` tree) or
from the `onMount` callback. Create/update use singular and plural forms.
```tsx
// src/hooks/useCanvasActions.ts
import { useEditor } from "tldraw";
export function useCanvasActions() {
const editor = useEditor();
function addTaskCard(title: string, x: number, y: number) {
editor.createShape({ type: "task-card", x, y, props: { title } });
}
// Export selection (or whole page) to a PNG blob.
// getSvg() was removed — use toImage(), getSvgString(), or getSvgElement().
async function exportPng() {
const ids = [...editor.getCurrentPageShapeIds()];
if (ids.length === 0) return null;
const { blob } = await editor.toImage(ids, { format: "png", scale: 2, background: true });
return blob;
}
function zoomToFit() {
editor.zoomToFit({ animation: { duration: 300 } });
}
return { addTaskCard, exportPng, zoomToFit };
}
```
### Snapshots (save & restore)
`getSnapshot`/`loadSnapshot` are top-level functions imported from `tldraw`.
`getSnapshot` returns `{ document, session }`; persist the `document` part.
```typescript
// src/persistence/snapshots.ts
import { Editor, getSnapshot, loadSnapshot } from "tldraw";
export async function saveToServer(editor: Editor, documentId: string) {
const { document } = getSnapshot(editor.store);
await fetch(`/api/boards/${documentId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(document),
});
}
export async function loadFromServer(editor: Editor, documentId: string) {
const document = await fetch(`/api/boards/${documentId}`).then((r) => r.json());
loadSnapshot(editor.store, { document });
}
// Debounce auto-save — canvas changes fire rapidly while drawing.
export function setupAutoSave(editor: Editor, documentId: string) {
let timer: ReturnType<typeof setTimeout>;
return editor.store.listen(
() => {
clearTimeout(timer);
timer = setTimeout(() => saveToServer(editor, documentId), 2000);
},
{ source: "user", scope: "document" }
);
}
```
### Multiplayer with @tldraw/sync
Official real-time collaboration uses the `useSync` hook from `@tldraw/sync`
talking to a websocket backend you host (tldraw ships a Cloudflare Durable
Objects server template). For a prototype, `useSyncDemo({ roomId })` connects to
tldraw's demo server with temporary rooms. There is no `@tldraw/yjs` package.
```tsx
// src/components/MultiplayerBoard.tsx
import { Tldraw, TLAssetStore, uniqueId } from "tldraw";
import { useSync } from "@tldraw/sync";
// `assets` is required: images and videos go to your own storage, the document keeps the URL.
// The /api/uploads and /api/connect routes are the ones tldraw's sync-cloudflare template serves.
const assets: TLAssetStore = {
async upload(_asset, file) {
const name = `${uniqueId()}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, "-");
const res = await fetch(`/api/uploads/${name}`, { method: "POST", body: file });
if (!res.ok) throw new Error(`Failed to upload asset: ${res.statusText}`);
return { src: `/api/uploads/${name}` };
},
resolve: (asset) => asset.props.src,
};
export function MultiplayerBoard({ roomId }: { roomId: string }) {
// useSync manages the websocket and reconciles document + presence state.
// An http(s) URI is upgraded to ws(s).
const store = useSync({ uri: `${window.location.origin}/api/connect/${roomId}`, assets });
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw store={store} />
</div>
);
}
```
## Installation
```bash
npm install tldraw # core SDK (includes the Editor and default UI)
# For multiplayer
npm install @tldraw/sync
```
## Examples
### Example 1: Embed a whiteboard with a custom shape
**User request:**
```
Add a tldraw canvas to our React dashboard and give it a "task-card" shape so
users can drop task cards on the board.
```
The agent runs `npm install tldraw`, creates a `Whiteboard` component that
imports `tldraw/tldraw.css` and renders `<Tldraw shapeUtils={[TaskCardShapeUtil]}
persistenceKey="sprint-board" />` inside a `position: fixed; inset: 0` container,
and defines `TaskCardShapeUtil extends BaseBoxShapeUtil`. Result: an infinite
canvas that persists to IndexedDB, with draggable task cards whose React
content stays interactive.
### Example 2: Export the current board as a PNG
**User request:**
```
Add a "Download PNG" button that exports whatever is on the tldraw canvas.
```
The agent wires a button to `editor.toImage([...editor.getCurrentPageShapeIds()],
{ format: "png", scale: 2, background: true })`, turns the returned `blob` into
an object URL, and triggers a download. Result: a 2x-resolution PNG of the whole
page; if the page is empty the handler returns early.
## Guidelines
1. **Production needs a license key** — since SDK 4, running tldraw in
production (HTTPS, non-loopback host, `NODE_ENV=production`) requires a
license key; without one the editor stops rendering after five seconds.
Commercial use needs a commercial license (no watermark; a free 100-day
trial key exists). The free hobby license is for non-commercial projects,
is granted on request and keeps the "made with tldraw" watermark.
Development needs no key. Pass the key with the `licenseKey` prop on
`<Tldraw>` or an env var such as `VITE_TLDRAW_LICENSE_KEY`.
2. **Container needs dimensions** — tldraw fills its parent; give it explicit
size (e.g. `position: fixed; inset: 0`) or it renders at zero height.
3. **Custom shapes for domain data** — represent tasks, nodes, or cards as
shapes instead of forcing everything into draw/text.
4. **Always implement `getIndicatorPath()`** — it returns the selection/hover
outline of a custom shape; SDK 5 no longer renders the JSX `indicator()`.
5. **`getSvg()` was removed** — use `editor.toImage()` (blob), or
`editor.getSvgString()` / `editor.getSvgElement()` for vector output.
SVG exports are resolution-independent and smaller than raster.
6. **Persist the `document` snapshot** — `getSnapshot(editor.store)` returns
`{ document, session }`; store `document` on your backend, keep `session`
per-client if you want to restore the viewport.
7. **Debounce auto-save** — `editor.store.listen` fires on every change; save
every 2–3 seconds, not per event.
8. **Multiplayer is `@tldraw/sync`, not Yjs** — the old community Yjs example is
not a maintained package; use `useSync` against a websocket server.
---
name: tldraw-efeb019b4300
description: >-
tldraw is a React SDK for building infinite-canvas apps — whiteboards, diagram
editors, and visual tools — with a shape system, camera controls, snapshots,
and real-time multiplayer. Use when embedding a collaborative canvas or
whiteboard in a React app, defining custom shapes, driving the editor
programmatically, exporting the canvas to an image, or adding multiplayer
sync. Note: production use requires a tldraw license key.
license: Apache-2.0
compatibility: "tldraw SDK 5 with React 18.2+ or 19.2.1+. Production use requires a tldraw license key."
metadata:
author: terminal-skills
version: "1.1.0"
category: development
tags: "canvas, whiteboard, drawing, collaboration, react"
repository: https://github.com/tldraw/tldraw
---
# tldraw — Infinite Canvas SDK
## Overview
tldraw is a React SDK for building infinite-canvas experiences: whiteboards,
diagram editors, and visual tools. You drop in the `<Tldraw>` component, extend
it with custom shapes that render as React/HTML or SVG, drive the canvas through
the `Editor` API, persist state with snapshots, and add real-time collaboration
with `@tldraw/sync`.
## Instructions
### Basic Setup
Install and embed a full-featured canvas. The container needs explicit
dimensions, and you must import the stylesheet.
```tsx
// src/components/Whiteboard.tsx
import { Tldraw } from "tldraw";
import "tldraw/tldraw.css";
export function Whiteboard() {
return (
<div style={{ position: "fixed", inset: 0 }}>
{/* persistenceKey saves to the browser's IndexedDB and syncs across tabs */}
<Tldraw persistenceKey="design-review-board" />
</div>
);
}
```
### Custom Shapes
Model your domain with a shape util. `BaseBoxShapeUtil` gives you a resizable
box; `component()` renders it (full React is allowed), `getIndicatorPath()`
returns the selection outline as a `Path2D` (it replaced the JSX `indicator()`
in SDK 5).
```tsx
// src/shapes/TaskCardShapeUtil.tsx
import { BaseBoxShapeUtil, HTMLContainer, T, TLShape } from "tldraw";
// Register the props under the shape's type name (a global registry: keep the name unique).
declare module "tldraw" {
export interface TLGlobalShapePropsMap {
"task-card": { title: string; assignee: string; priority: "low" | "high"; w: number; h: number };
}
}
type TaskCard = TLShape<"task-card">;
export class TaskCardShapeUtil extends BaseBoxShapeUtil<TaskCard> {
static override type = "task-card" as const;
static override props = { // validated by the store
title: T.string, assignee: T.string, priority: T.literalEnum("low", "high"), w: T.number, h: T.number,
};
getDefaultProps(): TaskCard["props"] {
return { title: "New task", assignee: "Unassigned", priority: "low", w: 280, h: 140 };
}
component(shape: TaskCard) {
const border = shape.props.priority === "high" ? "#ef4444" : "#4ade80";
return (
<HTMLContainer
style={{
padding: 12,
border: `2px solid ${border}`,
borderRadius: 8,
background: "white",
pointerEvents: "all", // let the HTML receive clicks
}}
>
<strong>{shape.props.title}</strong>
<div style={{ fontSize: 11, color: "#666" }}>{shape.props.assignee}</div>
</HTMLContainer>
);
}
getIndicatorPath(shape: TaskCard) {
const path = new Path2D();
path.roundRect(0, 0, shape.props.w, shape.props.h, 8);
return path;
}
}
```
Register it on the component:
```tsx
import { Tldraw } from "tldraw";
import { TaskCardShapeUtil } from "./shapes/TaskCardShapeUtil";
<Tldraw shapeUtils={[TaskCardShapeUtil]} persistenceKey="sprint-board" />;
```
### Driving the Editor API
Grab the `Editor` with the `useEditor()` hook (inside the `<Tldraw>` tree) or
from the `onMount` callback. Create/update use singular and plural forms.
```tsx
// src/hooks/useCanvasActions.ts
import { useEditor } from "tldraw";
export function useCanvasActions() {
const editor = useEditor();
function addTaskCard(title: string, x: number, y: number) {
editor.createShape({ type: "task-card", x, y, props: { title } });
}
// Export selection (or whole page) to a PNG blob.
// getSvg() was removed — use toImage(), getSvgString(), or getSvgElement().
async function exportPng() {
const ids = [...editor.getCurrentPageShapeIds()];
if (ids.length === 0) return null;
const { blob } = await editor.toImage(ids, { format: "png", scale: 2, background: true });
return blob;
}
function zoomToFit() {
editor.zoomToFit({ animation: { duration: 300 } });
}
return { addTaskCard, exportPng, zoomToFit };
}
```
### Snapshots (save & restore)
`getSnapshot`/`loadSnapshot` are top-level functions imported from `tldraw`.
`getSnapshot` returns `{ document, session }`; persist the `document` part.
```typescript
// src/persistence/snapshots.ts
import { Editor, getSnapshot, loadSnapshot } from "tldraw";
export async function saveToServer(editor: Editor, documentId: string) {
const { document } = getSnapshot(editor.store);
await fetch(`/api/boards/${documentId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(document),
});
}
export async function loadFromServer(editor: Editor, documentId: string) {
const document = await fetch(`/api/boards/${documentId}`).then((r) => r.json());
loadSnapshot(editor.store, { document });
}
// Debounce auto-save — canvas changes fire rapidly while drawing.
export function setupAutoSave(editor: Editor, documentId: string) {
let timer: ReturnType<typeof setTimeout>;
return editor.store.listen(
() => {
clearTimeout(timer);
timer = setTimeout(() => saveToServer(editor, documentId), 2000);
},
{ source: "user", scope: "document" }
);
}
```
### Multiplayer with @tldraw/sync
Official real-time collaboration uses the `useSync` hook from `@tldraw/sync`
talking to a websocket backend you host (tldraw ships a Cloudflare Durable
Objects server template). For a prototype, `useSyncDemo({ roomId })` connects to
tldraw's demo server with temporary rooms. There is no `@tldraw/yjs` package.
```tsx
// src/components/MultiplayerBoard.tsx
import { Tldraw, TLAssetStore, uniqueId } from "tldraw";
import { useSync } from "@tldraw/sync";
// `assets` is required: images and videos go to your own storage, the document keeps the URL.
// The /api/uploads and /api/connect routes are the ones tldraw's sync-cloudflare template serves.
const assets: TLAssetStore = {
async upload(_asset, file) {
const name = `${uniqueId()}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, "-");
const res = await fetch(`/api/uploads/${name}`, { method: "POST", body: file });
if (!res.ok) throw new Error(`Failed to upload asset: ${res.statusText}`);
return { src: `/api/uploads/${name}` };
},
resolve: (asset) => asset.props.src,
};
export function MultiplayerBoard({ roomId }: { roomId: string }) {
// useSync manages the websocket and reconciles document + presence state.
// An http(s) URI is upgraded to ws(s).
const store = useSync({ uri: `${window.location.origin}/api/connect/${roomId}`, assets });
return (
<div style={{ position: "fixed", inset: 0 }}>
<Tldraw store={store} />
</div>
);
}
```
## Installation
```bash
npm install tldraw # core SDK (includes the Editor and default UI)
# For multiplayer
npm install @tldraw/sync
```
## Examples
### Example 1: Embed a whiteboard with a custom shape
**User request:**
```
Add a tldraw canvas to our React dashboard and give it a "task-card" shape so
users can drop task cards on the board.
```
The agent runs `npm install tldraw`, creates a `Whiteboard` component that
imports `tldraw/tldraw.css` and renders `<Tldraw shapeUtils={[TaskCardShapeUtil]}
persistenceKey="sprint-board" />` inside a `position: fixed; inset: 0` container,
and defines `TaskCardShapeUtil extends BaseBoxShapeUtil`. Result: an infinite
canvas that persists to IndexedDB, with draggable task cards whose React
content stays interactive.
### Example 2: Export the current board as a PNG
**User request:**
```
Add a "Download PNG" button that exports whatever is on the tldraw canvas.
```
The agent wires a button to `editor.toImage([...editor.getCurrentPageShapeIds()],
{ format: "png", scale: 2, background: true })`, turns the returned `blob` into
an object URL, and triggers a download. Result: a 2x-resolution PNG of the whole
page; if the page is empty the handler returns early.
## Guidelines
1. **Production needs a license key** — since SDK 4, running tldraw in
production (HTTPS, non-loopback host, `NODE_ENV=production`) requires a
license key; without one the editor stops rendering after five seconds.
Commercial use needs a commercial license (no watermark; a free 100-day
trial key exists). The free hobby license is for non-commercial projects,
is granted on request and keeps the "made with tldraw" watermark.
Development needs no key. Pass the key with the `licenseKey` prop on
`<Tldraw>` or an env var such as `VITE_TLDRAW_LICENSE_KEY`.
2. **Container needs dimensions** — tldraw fills its parent; give it explicit
size (e.g. `position: fixed; inset: 0`) or it renders at zero height.
3. **Custom shapes for domain data** — represent tasks, nodes, or cards as
shapes instead of forcing everything into draw/text.
4. **Always implement `getIndicatorPath()`** — it returns the selection/hover
outline of a custom shape; SDK 5 no longer renders the JSX `indicator()`.
5. **`getSvg()` was removed** — use `editor.toImage()` (blob), or
`editor.getSvgString()` / `editor.getSvgElement()` for vector output.
SVG exports are resolution-independent and smaller than raster.
6. **Persist the `document` snapshot** — `getSnapshot(editor.store)` returns
`{ document, session }`; store `document` on your backend, keep `session`
per-client if you want to restore the viewport.
7. **Debounce auto-save** — `editor.store.listen` fires on every change; save
every 2–3 seconds, not per event.
8. **Multiplayer is `@tldraw/sync`, not Yjs** — the old community Yjs example is
not a maintained package; use `useSync` against a websocket server.
Platform guidance
Imported from the listed AgentSkill.sh category. Complete supplied package files and notices are retained. Required external tools and credentials are not installed. No bundled code was executed during import. Individual agent task behavior has not been tested.
Creates comprehensive frontend UI/UX designs including user interfaces, design systems, component libraries, responsive layouts, and accessibility implementations. Produces wireframes, mockups, design specifications, and implementation guidelines. Use when designing user interfaces, creating desi...
Expert in building scalable design systems with Atomic Design, design tokens, and theming infrastructure. Use when creating component libraries, implementing dark mode, or establishing typography and color systems. Covers multi-brand support, Storybook-driven development, and accessibility-first ...
Use when implementing Liquid Glass effects, reviewing UI for Liquid Glass adoption, debugging visual artifacts, optimizing performance, or requesting expert review of Liquid Glass implementation - provides comprehensive design principles, API patterns, and troubleshooting guidance from WWDC 2025....