Embedding tldraw in your application is straightforward and highly customizable. This guide covers everything from basic integration to advanced embedding patterns.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt
Use this file to discover all available pages before exploring further.
Basic embedding
React applications
The simplest way to embed tldraw in a React application:import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw />
</div>
)
}
The tldraw component requires a parent with defined dimensions. Use
position: fixed with inset: 0 or set explicit width/height values.With custom container
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function EmbeddedCanvas() {
return (
<div className="canvas-container">
<Tldraw />
</div>
)
}
.canvas-container {
width: 100%;
height: 600px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
Editor instance access
Access the editor instance for programmatic control:import { Editor, Tldraw } from 'tldraw'
import { useEffect, useState } from 'react'
export default function App() {
const [editor, setEditor] = useState<Editor | null>(null)
useEffect(() => {
if (!editor) return
// Editor is ready, perform initial setup
editor.createShape({
type: 'geo',
x: 100,
y: 100,
props: { w: 200, h: 150 },
})
}, [editor])
return (
<Tldraw
onMount={(editor) => {
setEditor(editor)
}}
/>
)
}
Using hooks
Access the editor in child components:import { Tldraw, useEditor } from 'tldraw'
function CustomButton() {
const editor = useEditor()
return (
<button onClick={() => {
editor.selectAll()
}}>
Select All
</button>
)
}
export default function App() {
return (
<Tldraw>
<CustomButton />
</Tldraw>
)
}
Headless editor
Use theEditor class directly for headless (no UI) integration:
import { Editor, createTLStore } from '@tldraw/editor'
const store = createTLStore({
shapeUtils: [],
bindingUtils: [],
})
const editor = new Editor({
store,
shapeUtils: [],
bindingUtils: [],
tools: [],
getContainer: () => document.body,
})
// Use editor programmatically
editor.createShape({
type: 'geo',
x: 0,
y: 0,
props: { w: 100, h: 100, geo: 'rectangle' },
})
const snapshot = editor.getSnapshot()
console.log(snapshot)
Use cases for headless editor
Use cases for headless editor
Headless editors are useful for:
- Server-side rendering and exports
- Automated testing
- Canvas manipulation without UI
- Background processing
- API integrations
- Command-line tools
Customizing the UI
Hide default UI
Embed just the canvas without toolbar and menus:import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function MinimalCanvas() {
return (
<Tldraw
hideUi
components={{
Toolbar: null,
HelpMenu: null,
MainMenu: null,
QuickActions: null,
PageMenu: null,
}}
/>
)
}
Custom toolbar
import { Tldraw, TldrawUiMenuItem, useTools } from 'tldraw'
function CustomToolbar() {
const tools = useTools()
return (
<div className="custom-toolbar">
<TldrawUiMenuItem
{...tools['select']}
isSelected={tools['select'].isSelected}
/>
<TldrawUiMenuItem
{...tools['draw']}
isSelected={tools['draw'].isSelected}
/>
<TldrawUiMenuItem
{...tools['eraser']}
isSelected={tools['eraser'].isSelected}
/>
</div>
)
}
export default function App() {
return (
<Tldraw
components={{
Toolbar: CustomToolbar,
}}
/>
)
}
Custom theme
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import './custom-theme.css'
export default function ThemedCanvas() {
return (
<div className="tldraw-custom-theme">
<Tldraw />
</div>
)
}
/* custom-theme.css */
.tldraw-custom-theme {
--color-background: #1e1e1e;
--color-muted: #2d2d2d;
--color-text: #ffffff;
--color-primary: #3b82f6;
}
.tldraw-custom-theme .tl-toolbar {
background: var(--color-muted);
border-radius: 12px;
}
Responsive embedding
Mobile-optimized
import { Tldraw } from 'tldraw'
import { useEffect, useState } from 'react'
export default function ResponsiveCanvas() {
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
}
checkMobile()
window.addEventListener('resize', checkMobile)
return () => window.removeEventListener('resize', checkMobile)
}, [])
return (
<Tldraw
forceMobile={isMobile}
components={{
// Simplify UI for mobile
HelpMenu: isMobile ? null : undefined,
PageMenu: isMobile ? null : undefined,
}}
/>
)
}
Adaptive layout
import { Tldraw } from 'tldraw'
import { useState, useEffect } from 'react'
function useContainerSize(ref: React.RefObject<HTMLDivElement>) {
const [size, setSize] = useState({ width: 0, height: 0 })
useEffect(() => {
if (!ref.current) return
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect
setSize({ width, height })
})
observer.observe(ref.current)
return () => observer.disconnect()
}, [ref])
return size
}
export default function AdaptiveCanvas() {
const containerRef = React.useRef<HTMLDivElement>(null)
const { width } = useContainerSize(containerRef)
return (
<div ref={containerRef} style={{ width: '100%', height: '600px' }}>
<Tldraw
components={{
// Hide UI elements when container is narrow
Toolbar: width < 400 ? null : undefined,
}}
/>
</div>
)
}
Persistence
LocalStorage persistence
import { Tldraw } from 'tldraw'
const PERSISTENCE_KEY = 'my-tldraw-canvas'
export default function PersistentCanvas() {
return (
<Tldraw
persistenceKey={PERSISTENCE_KEY}
/>
)
}
Custom persistence
import { Tldraw, Editor, TLStoreSnapshot } from 'tldraw'
import { useEffect, useState } from 'react'
export default function CustomPersistence() {
const [editor, setEditor] = useState<Editor | null>(null)
const [snapshot, setSnapshot] = useState<TLStoreSnapshot | null>(null)
// Load from API
useEffect(() => {
async function load() {
const response = await fetch('/api/canvas/123')
const data = await response.json()
setSnapshot(data.snapshot)
}
load()
}, [])
// Save to API
useEffect(() => {
if (!editor) return
const handleSave = async () => {
const snapshot = editor.getSnapshot()
await fetch('/api/canvas/123', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ snapshot }),
})
}
// Auto-save on changes
const dispose = editor.store.listen(handleSave)
return dispose
}, [editor])
return (
<Tldraw
onMount={setEditor}
snapshot={snapshot}
/>
)
}
Database integration
import { Tldraw, Editor } from 'tldraw'
import { useEffect, useState } from 'react'
import { supabase } from './supabase'
export default function DatabaseCanvas({ canvasId }: { canvasId: string }) {
const [editor, setEditor] = useState<Editor | null>(null)
const [snapshot, setSnapshot] = useState(null)
// Load from database
useEffect(() => {
async function loadCanvas() {
const { data } = await supabase
.from('canvases')
.select('snapshot')
.eq('id', canvasId)
.single()
if (data?.snapshot) {
setSnapshot(data.snapshot)
}
}
loadCanvas()
}, [canvasId])
// Auto-save to database
useEffect(() => {
if (!editor) return
let timeoutId: NodeJS.Timeout
const handleChange = () => {
clearTimeout(timeoutId)
timeoutId = setTimeout(async () => {
const snapshot = editor.getSnapshot()
await supabase
.from('canvases')
.upsert({
id: canvasId,
snapshot,
updated_at: new Date().toISOString(),
})
}, 1000) // Debounce saves by 1 second
}
const dispose = editor.store.listen(handleChange)
return () => {
dispose()
clearTimeout(timeoutId)
}
}, [editor, canvasId])
return <Tldraw onMount={setEditor} snapshot={snapshot} />
}
Constraints and permissions
Read-only mode
import { Tldraw, Editor } from 'tldraw'
import { useEffect } from 'react'
export default function ReadOnlyCanvas({ snapshot }) {
const handleMount = (editor: Editor) => {
// Disable all editing
editor.updateInstanceState({ isReadonly: true })
}
return (
<Tldraw
onMount={handleMount}
snapshot={snapshot}
components={{
Toolbar: null,
MainMenu: null,
}}
/>
)
}
Limited editing
import { Tldraw, Editor } from 'tldraw'
export default function LimitedCanvas() {
const handleMount = (editor: Editor) => {
// Only allow moving and selecting
editor.setCurrentTool('select')
// Prevent shape creation
const originalCreate = editor.createShape.bind(editor)
editor.createShape = (...args) => {
console.warn('Shape creation disabled')
return null
}
}
return <Tldraw onMount={handleMount} />
}
Multi-canvas embedding
Multiple canvases
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function MultiCanvas() {
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={{ height: '400px' }}>
<Tldraw persistenceKey="canvas-1" />
</div>
<div style={{ height: '400px' }}>
<Tldraw persistenceKey="canvas-2" />
</div>
</div>
)
}
Synchronized canvases
import { Tldraw, Editor, TLStoreSnapshot } from 'tldraw'
import { useState } from 'react'
export default function SyncedCanvases() {
const [snapshot, setSnapshot] = useState<TLStoreSnapshot | null>(null)
const handleChange = (editor: Editor) => {
const newSnapshot = editor.getSnapshot()
setSnapshot(newSnapshot)
}
return (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={{ height: '400px' }}>
<Tldraw
onMount={(editor) => {
editor.store.listen(() => handleChange(editor))
}}
/>
</div>
<div style={{ height: '400px' }}>
<Tldraw
snapshot={snapshot}
onMount={(editor) => {
editor.updateInstanceState({ isReadonly: true })
}}
/>
</div>
</div>
)
}
iframe embedding
Embed in iframe
<!-- parent.html -->
<!DOCTYPE html>
<html>
<head>
<title>tldraw Embedded</title>
</head>
<body>
<iframe
src="/tldraw-canvas.html"
width="100%"
height="600"
frameborder="0"
allow="clipboard-read; clipboard-write"
></iframe>
</body>
</html>
<!-- tldraw-canvas.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="/tldraw.css" />
<style>
body { margin: 0; overflow: hidden; }
#root { position: fixed; inset: 0; }
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
Cross-origin communication
// In iframe
import { Tldraw, Editor } from 'tldraw'
import { useEffect, useState } from 'react'
export default function IframeCanvas() {
const [editor, setEditor] = useState<Editor | null>(null)
useEffect(() => {
if (!editor) return
// Listen for messages from parent
window.addEventListener('message', (event) => {
if (event.data.type === 'CREATE_SHAPE') {
editor.createShape(event.data.shape)
}
})
// Send changes to parent
const dispose = editor.store.listen(() => {
window.parent.postMessage({
type: 'CANVAS_UPDATED',
snapshot: editor.getSnapshot(),
}, '*')
})
return dispose
}, [editor])
return <Tldraw onMount={setEditor} />
}
Next steps
Editor API
Learn about the Editor API
UI customization
Customize the tldraw UI
Persistence
Advanced persistence patterns
Multiplayer
Add real-time collaboration