We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
Guide28 min read

tldraw

tldraw is a free, open-source infinite canvas whiteboard for drawing diagrams, sketches, and wireframes with React embedding capabilities.

tldraw - Infinite Canvas Whiteboard dla Developerów

Czym jest tldraw?

tldraw to darmowy, w pełni open-source infinite canvas whiteboard, który można używać bezpośrednio w przeglądarce lub osadzić w aplikacjach React. Projekt został stworzony przez Steve'a Rubina i szybko zyskał popularność wśród deweloperów, designerów i zespołów szukających prostego, ale potężnego narzędzia do wizualizacji myśli, tworzenia diagramów i współpracy w czasie rzeczywistym.

W odróżnieniu od komercyjnych narzędzi jak Miro czy Figma, tldraw jest całkowicie darmowy i nie wymaga rejestracji. Wchodzisz na stronę, rysujesz i możesz natychmiast udostępnić link współpracownikom. Co więcej, jako biblioteka React, tldraw może być łatwo zintegrowany z własnymi aplikacjami - od narzędzi do dokumentacji, przez systemy zarządzania projektami, po edukacyjne platformy.

Szczególnie interesującą funkcją jest Make Real - integracja z AI, która pozwala narysować wireframe na tablicy, a następnie automatycznie wygenerować działający HTML/CSS na podstawie szkicu. To pokazuje potencjał tldraw jako narzędzia nie tylko do planowania, ale także do prototypowania.

Użycie online

Wejdź na tldraw.com i zacznij rysować - bez rejestracji, bez logowania, od razu.

Dlaczego tldraw?

Kluczowe zalety

  1. 100% darmowy - Open-source, Apache 2.0 License
  2. Bez rejestracji - Natychmiastowy dostęp do tablicy
  3. Infinite canvas - Nieograniczona przestrzeń do rysowania
  4. Real-time collaboration - Współpraca wielu osób na żywo
  5. Osadzanie w React - Łatwa integracja z aplikacjami
  6. Eksport - PNG, SVG, JSON, copy as image
  7. Multiplayer - Udostępnianie linków, cursor presence
  8. Customizacja - Rozszerzalne API dla własnych narzędzi
  9. Make Real - AI generuje kod z wireframów
  10. Self-hosting - Możliwość hostowania na własnym serwerze

tldraw vs konkurencja

CechatldrawExcalidrawMiroFigma
CenaDarmowyDarmowyFreemiumFreemium
Open-source✅ Apache 2.0✅ MIT
StylClean/SmoothHand-drawnPolishedProfessional
React library✅ Pełna✅ Podstawowa
Multiplayer
Self-hosting
AI integration✅ Make RealOgraniczone✅ AI features
Bez rejestracji
CustomizacjaWysokaŚredniaNiskaNiska

Funkcje i narzędzia

Narzędzia rysowania

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  TOOLBAR                                                │
├─────────────────────────────────────────────────────────┤
│  🔲 Select    - Zaznaczanie i przesuwanie elementów     │
│  ✏️ Draw      - Rysowanie odręczne                      │
│  ↗️ Arrow     - Strzałki z automatycznymi połączeniami  │
│  ⬜ Rectangle - Prostokąty i kwadraty                   │
│  ⭕ Ellipse   - Elipsy i okręgi                         │
│  △ Triangle  - Trójkąty                                │
│  ⬡ Polygon   - Wielokąty                               │
│  ⬢ Diamond   - Romby                                   │
│  ╱ Line      - Linie proste                            │
│  A Text      - Tekst                                   │
│  📝 Note     - Sticky notes                            │
│  🖼️ Image    - Obrazy                                  │
│  📦 Frame    - Ramki do grupowania                     │
│  ✂️ Crop     - Przycinanie obrazów                     │
│  🎨 Highlight - Zakreślacz                             │
│  🔗 Embed    - Osadzanie zewnętrznych treści           │
└─────────────────────────────────────────────────────────┘

Właściwości elementów

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  STYLE OPTIONS                                          │
├─────────────────────────────────────────────────────────┤
│  Color:     🔵 Blue  🟢 Green  🔴 Red  🟡 Yellow  ⚫ Black │
│  Fill:      None | Solid | Pattern | Semi              │
│  Dash:      Solid | Dashed | Dotted                    │
│  Size:      S | M | L | XL                              │
│  Font:      Sans | Serif | Mono | Draw                  │
│  Opacity:   0% ────────────────────────── 100%         │
└─────────────────────────────────────────────────────────┘

Skróty klawiszowe

Code
Bash
# Nawigacja
Space + Drag     # Przesuwanie widoku (pan)
Scroll           # Zoom
Ctrl/Cmd + 0     # Zoom to fit
Ctrl/Cmd + 1     # Zoom 100%
Ctrl/Cmd + 2     # Zoom to selection

# Narzędzia
V                # Select
D                # Draw
A                # Arrow
R                # Rectangle
O                # Ellipse
T                # Text
N                # Note
F                # Frame

# Akcje
Ctrl/Cmd + Z     # Undo
Ctrl/Cmd + Y     # Redo
Ctrl/Cmd + C     # Copy
Ctrl/Cmd + V     # Paste
Ctrl/Cmd + D     # Duplicate
Delete           # Usuń
Ctrl/Cmd + G     # Grupuj
Ctrl/Cmd + Shift + G  # Rozgrupuj

# Eksport
Ctrl/Cmd + Shift + C  # Copy as PNG
Ctrl/Cmd + Shift + E  # Export

Współpraca

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  MULTIPLAYER FEATURES                                   │
├─────────────────────────────────────────────────────────┤
│  🔗 Share Link    - Generuj link do współpracy          │
│  👥 Cursors       - Widzisz kursory innych osób         │
│  💬 Comments      - Dodawaj komentarze                  │
│  👤 Presence      - Lista osób na tablicy               │
│  🔒 Read-only     - Linki tylko do odczytu             │
│  📱 Mobile        - Działa na telefonach               │
└─────────────────────────────────────────────────────────┘

# Udostępnianie
1. Kliknij "Share" w prawym górnym rogu
2. Skopiuj link
3. Wyślij współpracownikom
4. Real-time collaboration!

Osadzanie w React

Podstawowa instalacja

Code
Bash
npm install tldraw
# lub
pnpm add tldraw
# lub
yarn add tldraw

Podstawowy komponent

TScomponents/Whiteboard.tsx
TypeScript
// components/Whiteboard.tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export function Whiteboard() {
  return (
    <div style={{ position: 'fixed', inset: 0 }}>
      <Tldraw />
    </div>
  )
}

Z persystencją

TScomponents/PersistentWhiteboard.tsx
TypeScript
// components/PersistentWhiteboard.tsx
import { Tldraw, createTLStore, defaultShapeUtils, TLDocument } from 'tldraw'
import 'tldraw/tldraw.css'
import { useEffect, useState } from 'react'

const STORAGE_KEY = 'my-whiteboard'

export function PersistentWhiteboard() {
  const [store] = useState(() => {
    const store = createTLStore({ shapeUtils: defaultShapeUtils })

    // Załaduj zapisany stan
    const saved = localStorage.getItem(STORAGE_KEY)
    if (saved) {
      const snapshot = JSON.parse(saved)
      store.loadSnapshot(snapshot)
    }

    return store
  })

  useEffect(() => {
    // Zapisuj zmiany
    const unsubscribe = store.listen(() => {
      const snapshot = store.getSnapshot()
      localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot))
    })

    return unsubscribe
  }, [store])

  return (
    <div style={{ height: '100vh' }}>
      <Tldraw store={store} />
    </div>
  )
}

Customowe narzędzia

TScomponents/CustomToolWhiteboard.tsx
TypeScript
// components/CustomToolWhiteboard.tsx
import {
  Tldraw,
  TLUiOverrides,
  TLUiActionsContextType,
  DefaultToolbar,
  useTools,
  TldrawUiMenuItem
} from 'tldraw'
import 'tldraw/tldraw.css'

// Własne narzędzie - stempel
const stampTool = {
  id: 'stamp',
  icon: 'stamp-icon',
  label: 'Stamp',
  onSelect: (editor: any) => {
    // Logika narzędzia
  }
}

// Override UI
const uiOverrides: TLUiOverrides = {
  tools: (editor, tools) => {
    return {
      ...tools,
      stamp: {
        id: 'stamp',
        icon: 'stamp',
        label: 'Stamp',
        kbd: 's',
        onSelect: () => {
          editor.setCurrentTool('stamp')
        }
      }
    }
  },
  actions: (editor, actions) => {
    return {
      ...actions,
      'clear-all': {
        id: 'clear-all',
        label: 'Clear All',
        kbd: 'shift+c',
        onSelect: () => {
          editor.deleteAll()
        }
      }
    }
  }
}

// Własny toolbar
function CustomToolbar() {
  const tools = useTools()

  return (
    <DefaultToolbar>
      <TldrawUiMenuItem {...tools['select']} />
      <TldrawUiMenuItem {...tools['draw']} />
      <TldrawUiMenuItem {...tools['arrow']} />
      <TldrawUiMenuItem {...tools['rectangle']} />
      <TldrawUiMenuItem {...tools['text']} />
      {/* Własny przycisk */}
      <TldrawUiMenuItem {...tools['stamp']} />
    </DefaultToolbar>
  )
}

export function CustomToolWhiteboard() {
  return (
    <div style={{ height: '100vh' }}>
      <Tldraw
        overrides={uiOverrides}
        components={{
          Toolbar: CustomToolbar
        }}
      />
    </div>
  )
}

Własne kształty (Custom Shapes)

TSshapes/StarShape.tsx
TypeScript
// shapes/StarShape.tsx
import {
  BaseBoxShapeUtil,
  Geometry2d,
  HTMLContainer,
  RecordProps,
  T,
  TLBaseShape,
  Polygon2d,
  Vec,
} from 'tldraw'

// Definicja typu kształtu
type IStarShape = TLBaseShape<
  'star',
  {
    w: number
    h: number
    points: number
    color: string
  }
>

// Walidacja props
const starShapeProps: RecordProps<IStarShape> = {
  w: T.number,
  h: T.number,
  points: T.number,
  color: T.string,
}

// Utility do generowania punktów gwiazdy
function getStarPoints(cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) {
  const points: Vec[] = []
  const step = Math.PI / spikes

  for (let i = 0; i < 2 * spikes; i++) {
    const radius = i % 2 === 0 ? outerRadius : innerRadius
    const angle = i * step - Math.PI / 2
    points.push(new Vec(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)))
  }

  return points
}

// Shape Util
export class StarShapeUtil extends BaseBoxShapeUtil<IStarShape> {
  static override type = 'star' as const
  static override props = starShapeProps

  override getDefaultProps(): IStarShape['props'] {
    return {
      w: 100,
      h: 100,
      points: 5,
      color: '#ffd700',
    }
  }

  override getGeometry(shape: IStarShape): Geometry2d {
    const points = getStarPoints(
      shape.props.w / 2,
      shape.props.h / 2,
      shape.props.points,
      Math.min(shape.props.w, shape.props.h) / 2,
      Math.min(shape.props.w, shape.props.h) / 4
    )
    return new Polygon2d({ points, isFilled: true })
  }

  override component(shape: IStarShape) {
    const points = getStarPoints(
      shape.props.w / 2,
      shape.props.h / 2,
      shape.props.points,
      Math.min(shape.props.w, shape.props.h) / 2,
      Math.min(shape.props.w, shape.props.h) / 4
    )

    const pathData = points
      .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
      .join(' ') + ' Z'

    return (
      <HTMLContainer>
        <svg width={shape.props.w} height={shape.props.h}>
          <path
            d={pathData}
            fill={shape.props.color}
            stroke="black"
            strokeWidth={2}
          />
        </svg>
      </HTMLContainer>
    )
  }

  override indicator(shape: IStarShape) {
    const points = getStarPoints(
      shape.props.w / 2,
      shape.props.h / 2,
      shape.props.points,
      Math.min(shape.props.w, shape.props.h) / 2,
      Math.min(shape.props.w, shape.props.h) / 4
    )

    const pathData = points
      .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
      .join(' ') + ' Z'

    return <path d={pathData} />
  }
}

// Użycie
import { Tldraw, defaultShapeUtils } from 'tldraw'
import { StarShapeUtil } from './shapes/StarShape'

const customShapeUtils = [...defaultShapeUtils, StarShapeUtil]

function App() {
  return (
    <Tldraw shapeUtils={customShapeUtils} />
  )
}

Multiplayer z Yjs

TScomponents/MultiplayerWhiteboard.tsx
TypeScript
// components/MultiplayerWhiteboard.tsx
import { Tldraw, useSync } from 'tldraw'
import { useSyncDemo } from '@tldraw/sync'
import 'tldraw/tldraw.css'

export function MultiplayerWhiteboard({ roomId }: { roomId: string }) {
  // Użyj tldraw sync demo server
  const store = useSyncDemo({
    roomId,
    userPreferences: {
      name: 'User',
      color: '#ff0000',
    }
  })

  return (
    <div style={{ height: '100vh' }}>
      <Tldraw
        store={store}
        autoFocus
      />
    </div>
  )
}
Code
TypeScript
// Własny backend z Yjs
import { Tldraw, TLStore, TLStoreWithStatus } from 'tldraw'
import { useYjsStore } from './useYjsStore'

function useYjsStore(roomId: string): TLStoreWithStatus {
  // Implementacja Yjs synchronizacji
  // Zobacz dokumentację tldraw dla szczegółów
}

export function CustomMultiplayerWhiteboard({ roomId }: { roomId: string }) {
  const storeWithStatus = useYjsStore(roomId)

  return (
    <div style={{ height: '100vh' }}>
      <Tldraw store={storeWithStatus} />
    </div>
  )
}

Make Real - AI Integration

Jak działa Make Real

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  MAKE REAL WORKFLOW                                     │
├─────────────────────────────────────────────────────────┤
│  1. Narysuj wireframe na tablicy                        │
│     ┌─────────────────────┐                             │
│     │  [Logo]   Nav  Nav  │                             │
│     │                     │                             │
│     │   Hero Section      │                             │
│     │   [ Button ]        │                             │
│     │                     │                             │
│     └─────────────────────┘                             │
│                                                         │
│  2. Zaznacz elementy                                    │
│                                                         │
│  3. Kliknij "Make Real"                                 │
│                                                         │
│  4. AI generuje HTML/CSS/JS                             │
│     ```html                                             │
│     <nav class="flex justify-between p-4">              │
│       <div class="logo">Logo</div>                      │
│       <div class="nav-links">...</div>                  │
│     </nav>                                              │
│     ...                                                 │
│     ```                                                 │
│                                                         │
│  5. Podgląd w przeglądarce!                            │
└─────────────────────────────────────────────────────────┘

Implementacja Make Real

Code
TypeScript
// Własna implementacja Make Real
import { Tldraw, Editor, TLShapeId } from 'tldraw'
import Anthropic from '@anthropic-ai/sdk'

const anthropic = new Anthropic()

async function makeReal(editor: Editor, selectedIds: TLShapeId[]) {
  // 1. Eksportuj zaznaczenie jako obraz
  const blob = await editor.exportShapes(selectedIds, {
    type: 'png',
    scale: 2,
  })

  // 2. Konwertuj na base64
  const base64 = await blobToBase64(blob)

  // 3. Wyślij do Claude z promptem
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-5-20241022',
    max_tokens: 4096,
    messages: [{
      role: 'user',
      content: [
        {
          type: 'image',
          source: {
            type: 'base64',
            media_type: 'image/png',
            data: base64
          }
        },
        {
          type: 'text',
          text: `Przekształć ten wireframe w kod HTML z Tailwind CSS.
                 Zwróć tylko kod HTML, bez wyjaśnień.
                 Kod powinien być responsywny i semantyczny.`
        }
      ]
    }]
  })

  // 4. Wyodrębnij HTML z odpowiedzi
  const html = extractCode(response.content[0].text)

  // 5. Wyświetl preview
  openPreview(html)
}

// Dodaj przycisk do tldraw
const uiOverrides = {
  actions: (editor: Editor, actions: any) => ({
    ...actions,
    'make-real': {
      id: 'make-real',
      label: 'Make Real',
      kbd: 'shift+m',
      onSelect: () => {
        const selectedIds = editor.getSelectedShapeIds()
        if (selectedIds.length > 0) {
          makeReal(editor, selectedIds)
        }
      }
    }
  })
}

Self-hosting

Docker

docker-compose.yml
YAML
# docker-compose.yml
version: '3.8'

services:
  tldraw:
    image: tldraw/tldraw:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    volumes:
      - tldraw_data:/app/data

volumes:
  tldraw_data:
Code
Bash
# Uruchomienie
docker-compose up -d

# Dostęp
# http://localhost:3000

Własna instancja z Next.js

Code
Bash
# Stwórz projekt Next.js z tldraw
npx create-next-app@latest my-tldraw --typescript
cd my-tldraw
npm install tldraw
TSapp/page.tsx
TypeScript
// app/page.tsx
'use client'

import dynamic from 'next/dynamic'

const Tldraw = dynamic(
  async () => (await import('tldraw')).Tldraw,
  { ssr: false }
)

import 'tldraw/tldraw.css'

export default function Home() {
  return (
    <div style={{ position: 'fixed', inset: 0 }}>
      <Tldraw />
    </div>
  )
}

Backend z Cloudflare Workers

TSworkers/tldraw-sync.ts
TypeScript
// workers/tldraw-sync.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('*', cors())

// Przechowywanie dokumentów
const documents = new Map<string, any>()

app.get('/room/:id', async (c) => {
  const id = c.req.param('id')
  const doc = documents.get(id) || { shapes: [], bindings: [] }
  return c.json(doc)
})

app.put('/room/:id', async (c) => {
  const id = c.req.param('id')
  const data = await c.req.json()
  documents.set(id, data)
  return c.json({ success: true })
})

export default app

Eksport i import

Programistyczny eksport

Code
TypeScript
import { Tldraw, Editor } from 'tldraw'

function ExportButtons({ editor }: { editor: Editor }) {

  const exportPng = async () => {
    const shapeIds = editor.getCurrentPageShapeIds()
    const blob = await editor.exportShapes([...shapeIds], {
      type: 'png',
      scale: 2,
      background: true
    })

    // Download
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'whiteboard.png'
    a.click()
  }

  const exportSvg = async () => {
    const shapeIds = editor.getCurrentPageShapeIds()
    const svg = await editor.exportShapesSvg([...shapeIds], {
      background: true
    })

    const blob = new Blob([svg.innerHTML], { type: 'image/svg+xml' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'whiteboard.svg'
    a.click()
  }

  const exportJson = () => {
    const snapshot = editor.store.getSnapshot()
    const json = JSON.stringify(snapshot, null, 2)

    const blob = new Blob([json], { type: 'application/json' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'whiteboard.json'
    a.click()
  }

  return (
    <div className="export-buttons">
      <button onClick={exportPng}>Export PNG</button>
      <button onClick={exportSvg}>Export SVG</button>
      <button onClick={exportJson}>Export JSON</button>
    </div>
  )
}

Import z JSON

Code
TypeScript
function ImportButton({ editor }: { editor: Editor }) {
  const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    const text = await file.text()
    const snapshot = JSON.parse(text)

    editor.store.loadSnapshot(snapshot)
  }

  return (
    <input
      type="file"
      accept=".json"
      onChange={handleImport}
    />
  )
}

Use Cases

Dokumentacja techniczna

Code
TypeScript
// Embed w dokumentacji (np. Docusaurus)
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export function DiagramEmbed({ snapshotUrl }: { snapshotUrl: string }) {
  const [snapshot, setSnapshot] = useState(null)

  useEffect(() => {
    fetch(snapshotUrl)
      .then(r => r.json())
      .then(setSnapshot)
  }, [snapshotUrl])

  if (!snapshot) return <div>Loading...</div>

  return (
    <div style={{ height: 400, border: '1px solid #ccc' }}>
      <Tldraw
        snapshot={snapshot}
        hideUi
        // Tylko podgląd, bez edycji
      />
    </div>
  )
}

Brainstorming z zespołem

Code
TypeScript
// Pokój do burzy mózgów
export function BrainstormRoom({ teamId }: { teamId: string }) {
  const store = useSyncDemo({
    roomId: `brainstorm-${teamId}`,
    userPreferences: {
      name: currentUser.name,
      color: currentUser.color
    }
  })

  return (
    <div className="h-screen">
      <header className="p-4 bg-gray-100 flex justify-between">
        <h1>Team Brainstorm</h1>
        <ParticipantsList />
      </header>
      <div className="h-[calc(100vh-60px)]">
        <Tldraw store={store} />
      </div>
    </div>
  )
}

Edukacja online

Code
TypeScript
// Tablica nauczyciela z możliwością nagrywania
export function TeacherWhiteboard() {
  const editorRef = useRef<Editor | null>(null)
  const [isRecording, setIsRecording] = useState(false)
  const [history, setHistory] = useState<any[]>([])

  const startRecording = () => {
    setIsRecording(true)
    // Zapisuj każdą zmianę
    editorRef.current?.store.listen((update) => {
      setHistory(h => [...h, { ...update, timestamp: Date.now() }])
    })
  }

  const playback = () => {
    // Odtwórz nagranie
    history.forEach((update, i) => {
      setTimeout(() => {
        editorRef.current?.store.applyDiff(update)
      }, update.timestamp - history[0].timestamp)
    })
  }

  return (
    <div>
      <div className="controls">
        <button onClick={startRecording}>
          {isRecording ? '⏹ Stop' : '⏺ Record'}
        </button>
        <button onClick={playback}>▶ Playback</button>
      </div>
      <Tldraw
        onMount={(editor) => { editorRef.current = editor }}
      />
    </div>
  )
}

Cennik

PlanCenaZawiera
Open Source$0Pełna biblioteka, self-hosting, unlimited
tldraw.com$0Darmowa tablica online, multiplayer
EnterpriseKontaktDedykowane wsparcie, SLA, custom features

tldraw jest 100% darmowy na licencji Apache 2.0.

FAQ - Najczęściej zadawane pytania

Czy mogę używać tldraw komercyjnie?

Tak, licencja Apache 2.0 pozwala na komercyjne użycie bez ograniczeń.

Jak zapisać stan tablicy?

Użyj editor.store.getSnapshot() do eksportu i editor.store.loadSnapshot() do importu. Możesz zapisać w localStorage, bazie danych lub wysłać na serwer.

Czy tldraw działa offline?

Tak, podstawowa funkcjonalność działa offline. Dla multiplayer potrzebujesz połączenia.

Jak dodać własne kształty?

Stwórz klasę rozszerzającą BaseBoxShapeUtil lub BaseShapeUtil, zdefiniuj type, props, component i indicator, a następnie przekaż do shapeUtils.

Czy tldraw obsługuje touch/stylus?

Tak, tldraw ma pełne wsparcie dla touch, stylus i pressure sensitivity (nacisk pióra).

Jak zintegrować z backendem?

Użyj oficjalnego @tldraw/sync lub zaimplementuj własną synchronizację z Yjs, WebSocket lub REST API.

Czy mogę ukryć UI?

Tak, użyj hideUi prop lub components prop do selektywnego ukrywania elementów.

Podsumowanie

tldraw to potężne, darmowe narzędzie do wizualizacji:

  • 100% open-source - Apache 2.0, bez ukrytych kosztów
  • Infinite canvas - Nieograniczona przestrzeń do rysowania
  • React library - Łatwa integracja z aplikacjami
  • Real-time multiplayer - Współpraca w czasie rzeczywistym
  • Make Real - AI generuje kod z wireframów
  • Customizable - Własne narzędzia, kształty, UI
  • Self-hostable - Pełna kontrola nad danymi

tldraw to idealny wybór dla teamów szukających darmowej, rozszerzalnej tablicy do diagramów, wireframów i współpracy.


tldraw - Infinite Canvas Whiteboard for developers

What is tldraw?

tldraw is a free, fully open-source infinite canvas whiteboard that you can use directly in the browser or embed in React applications. The project was created by Steve Rubin and quickly gained popularity among developers, designers, and teams looking for a simple yet powerful tool for visualizing ideas, creating diagrams, and collaborating in real time.

Unlike commercial tools such as Miro or Figma, tldraw is completely free and requires no registration. You visit the website, start drawing, and can immediately share the link with collaborators. Moreover, as a React library, tldraw can be easily integrated into your own applications - from documentation tools, through project management systems, to educational platforms.

A particularly interesting feature is Make Real - an AI integration that lets you draw a wireframe on the whiteboard and then automatically generate working HTML/CSS based on the sketch. This demonstrates tldraw's potential as a tool not just for planning, but also for prototyping.

Use online

Head over to tldraw.com and start drawing - no registration, no login, right away.

Why tldraw?

Key advantages

  1. 100% free - Open-source, Apache 2.0 License
  2. No registration - Instant access to the whiteboard
  3. Infinite canvas - Unlimited drawing space
  4. Real-time collaboration - Multiple people working live together
  5. React embedding - Easy integration with applications
  6. Export - PNG, SVG, JSON, copy as image
  7. Multiplayer - Link sharing, cursor presence
  8. Customization - Extensible API for custom tools
  9. Make Real - AI generates code from wireframes
  10. Self-hosting - Ability to host on your own server

tldraw vs the competition

FeaturetldrawExcalidrawMiroFigma
PriceFreeFreeFreemiumFreemium
Open-source✅ Apache 2.0✅ MIT
StyleClean/SmoothHand-drawnPolishedProfessional
React library✅ Full✅ Basic
Multiplayer
Self-hosting
AI integration✅ Make RealLimited✅ AI features
No registration
CustomizationHighMediumLowLow

Features and tools

Drawing tools

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  TOOLBAR                                                │
├─────────────────────────────────────────────────────────┤
│  🔲 Select    - Select and move elements                │
│  ✏️ Draw      - Freehand drawing                        │
│  ↗️ Arrow     - Arrows with automatic connections       │
│  ⬜ Rectangle - Rectangles and squares                  │
│  ⭕ Ellipse   - Ellipses and circles                    │
│  △ Triangle  - Triangles                               │
│  ⬡ Polygon   - Polygons                                │
│  ⬢ Diamond   - Diamonds                                │
│  ╱ Line      - Straight lines                          │
│  A Text      - Text                                    │
│  📝 Note     - Sticky notes                            │
│  🖼️ Image    - Images                                  │
│  📦 Frame    - Frames for grouping                     │
│  ✂️ Crop     - Image cropping                          │
│  🎨 Highlight - Highlighter                            │
│  🔗 Embed    - Embedding external content              │
└─────────────────────────────────────────────────────────┘

Element properties

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  STYLE OPTIONS                                          │
├─────────────────────────────────────────────────────────┤
│  Color:     🔵 Blue  🟢 Green  🔴 Red  🟡 Yellow  ⚫ Black │
│  Fill:      None | Solid | Pattern | Semi              │
│  Dash:      Solid | Dashed | Dotted                    │
│  Size:      S | M | L | XL                              │
│  Font:      Sans | Serif | Mono | Draw                  │
│  Opacity:   0% ────────────────────────── 100%         │
└─────────────────────────────────────────────────────────┘

Keyboard shortcuts

Code
Bash
# Navigation
Space + Drag     # Pan the view
Scroll           # Zoom
Ctrl/Cmd + 0     # Zoom to fit
Ctrl/Cmd + 1     # Zoom 100%
Ctrl/Cmd + 2     # Zoom to selection

# Tools
V                # Select
D                # Draw
A                # Arrow
R                # Rectangle
O                # Ellipse
T                # Text
N                # Note
F                # Frame

# Actions
Ctrl/Cmd + Z     # Undo
Ctrl/Cmd + Y     # Redo
Ctrl/Cmd + C     # Copy
Ctrl/Cmd + V     # Paste
Ctrl/Cmd + D     # Duplicate
Delete           # Delete
Ctrl/Cmd + G     # Group
Ctrl/Cmd + Shift + G  # Ungroup

# Export
Ctrl/Cmd + Shift + C  # Copy as PNG
Ctrl/Cmd + Shift + E  # Export

Collaboration

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  MULTIPLAYER FEATURES                                   │
├─────────────────────────────────────────────────────────┤
│  🔗 Share Link    - Generate a collaboration link       │
│  👥 Cursors       - See other people's cursors          │
│  💬 Comments      - Add comments                        │
│  👤 Presence      - List of people on the board         │
│  🔒 Read-only     - Read-only links                    │
│  📱 Mobile        - Works on phones                    │
└─────────────────────────────────────────────────────────┘

# Sharing
1. Click "Share" in the top right corner
2. Copy the link
3. Send it to collaborators
4. Real-time collaboration!

Embedding in React

Basic installation

Code
Bash
npm install tldraw
# or
pnpm add tldraw
# or
yarn add tldraw

Basic component

TScomponents/Whiteboard.tsx
TypeScript
// components/Whiteboard.tsx
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export function Whiteboard() {
  return (
    <div style={{ position: 'fixed', inset: 0 }}>
      <Tldraw />
    </div>
  )
}

With persistence

TScomponents/PersistentWhiteboard.tsx
TypeScript
// components/PersistentWhiteboard.tsx
import { Tldraw, createTLStore, defaultShapeUtils, TLDocument } from 'tldraw'
import 'tldraw/tldraw.css'
import { useEffect, useState } from 'react'

const STORAGE_KEY = 'my-whiteboard'

export function PersistentWhiteboard() {
  const [store] = useState(() => {
    const store = createTLStore({ shapeUtils: defaultShapeUtils })

    const saved = localStorage.getItem(STORAGE_KEY)
    if (saved) {
      const snapshot = JSON.parse(saved)
      store.loadSnapshot(snapshot)
    }

    return store
  })

  useEffect(() => {
    const unsubscribe = store.listen(() => {
      const snapshot = store.getSnapshot()
      localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot))
    })

    return unsubscribe
  }, [store])

  return (
    <div style={{ height: '100vh' }}>
      <Tldraw store={store} />
    </div>
  )
}

Custom tools

TScomponents/CustomToolWhiteboard.tsx
TypeScript
// components/CustomToolWhiteboard.tsx
import {
  Tldraw,
  TLUiOverrides,
  TLUiActionsContextType,
  DefaultToolbar,
  useTools,
  TldrawUiMenuItem
} from 'tldraw'
import 'tldraw/tldraw.css'

const stampTool = {
  id: 'stamp',
  icon: 'stamp-icon',
  label: 'Stamp',
  onSelect: (editor: any) => {
  }
}

const uiOverrides: TLUiOverrides = {
  tools: (editor, tools) => {
    return {
      ...tools,
      stamp: {
        id: 'stamp',
        icon: 'stamp',
        label: 'Stamp',
        kbd: 's',
        onSelect: () => {
          editor.setCurrentTool('stamp')
        }
      }
    }
  },
  actions: (editor, actions) => {
    return {
      ...actions,
      'clear-all': {
        id: 'clear-all',
        label: 'Clear All',
        kbd: 'shift+c',
        onSelect: () => {
          editor.deleteAll()
        }
      }
    }
  }
}

function CustomToolbar() {
  const tools = useTools()

  return (
    <DefaultToolbar>
      <TldrawUiMenuItem {...tools['select']} />
      <TldrawUiMenuItem {...tools['draw']} />
      <TldrawUiMenuItem {...tools['arrow']} />
      <TldrawUiMenuItem {...tools['rectangle']} />
      <TldrawUiMenuItem {...tools['text']} />
      <TldrawUiMenuItem {...tools['stamp']} />
    </DefaultToolbar>
  )
}

export function CustomToolWhiteboard() {
  return (
    <div style={{ height: '100vh' }}>
      <Tldraw
        overrides={uiOverrides}
        components={{
          Toolbar: CustomToolbar
        }}
      />
    </div>
  )
}

Custom shapes

TSshapes/StarShape.tsx
TypeScript
// shapes/StarShape.tsx
import {
  BaseBoxShapeUtil,
  Geometry2d,
  HTMLContainer,
  RecordProps,
  T,
  TLBaseShape,
  Polygon2d,
  Vec,
} from 'tldraw'

type IStarShape = TLBaseShape<
  'star',
  {
    w: number
    h: number
    points: number
    color: string
  }
>

const starShapeProps: RecordProps<IStarShape> = {
  w: T.number,
  h: T.number,
  points: T.number,
  color: T.string,
}

function getStarPoints(cx: number, cy: number, spikes: number, outerRadius: number, innerRadius: number) {
  const points: Vec[] = []
  const step = Math.PI / spikes

  for (let i = 0; i < 2 * spikes; i++) {
    const radius = i % 2 === 0 ? outerRadius : innerRadius
    const angle = i * step - Math.PI / 2
    points.push(new Vec(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle)))
  }

  return points
}

export class StarShapeUtil extends BaseBoxShapeUtil<IStarShape> {
  static override type = 'star' as const
  static override props = starShapeProps

  override getDefaultProps(): IStarShape['props'] {
    return {
      w: 100,
      h: 100,
      points: 5,
      color: '#ffd700',
    }
  }

  override getGeometry(shape: IStarShape): Geometry2d {
    const points = getStarPoints(
      shape.props.w / 2,
      shape.props.h / 2,
      shape.props.points,
      Math.min(shape.props.w, shape.props.h) / 2,
      Math.min(shape.props.w, shape.props.h) / 4
    )
    return new Polygon2d({ points, isFilled: true })
  }

  override component(shape: IStarShape) {
    const points = getStarPoints(
      shape.props.w / 2,
      shape.props.h / 2,
      shape.props.points,
      Math.min(shape.props.w, shape.props.h) / 2,
      Math.min(shape.props.w, shape.props.h) / 4
    )

    const pathData = points
      .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
      .join(' ') + ' Z'

    return (
      <HTMLContainer>
        <svg width={shape.props.w} height={shape.props.h}>
          <path
            d={pathData}
            fill={shape.props.color}
            stroke="black"
            strokeWidth={2}
          />
        </svg>
      </HTMLContainer>
    )
  }

  override indicator(shape: IStarShape) {
    const points = getStarPoints(
      shape.props.w / 2,
      shape.props.h / 2,
      shape.props.points,
      Math.min(shape.props.w, shape.props.h) / 2,
      Math.min(shape.props.w, shape.props.h) / 4
    )

    const pathData = points
      .map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`)
      .join(' ') + ' Z'

    return <path d={pathData} />
  }
}

import { Tldraw, defaultShapeUtils } from 'tldraw'
import { StarShapeUtil } from './shapes/StarShape'

const customShapeUtils = [...defaultShapeUtils, StarShapeUtil]

function App() {
  return (
    <Tldraw shapeUtils={customShapeUtils} />
  )
}

Multiplayer with Yjs

TScomponents/MultiplayerWhiteboard.tsx
TypeScript
// components/MultiplayerWhiteboard.tsx
import { Tldraw, useSync } from 'tldraw'
import { useSyncDemo } from '@tldraw/sync'
import 'tldraw/tldraw.css'

export function MultiplayerWhiteboard({ roomId }: { roomId: string }) {
  const store = useSyncDemo({
    roomId,
    userPreferences: {
      name: 'User',
      color: '#ff0000',
    }
  })

  return (
    <div style={{ height: '100vh' }}>
      <Tldraw
        store={store}
        autoFocus
      />
    </div>
  )
}
Code
TypeScript
// Custom backend with Yjs
import { Tldraw, TLStore, TLStoreWithStatus } from 'tldraw'
import { useYjsStore } from './useYjsStore'

function useYjsStore(roomId: string): TLStoreWithStatus {
  // Yjs synchronization implementation
  // See tldraw documentation for details
}

export function CustomMultiplayerWhiteboard({ roomId }: { roomId: string }) {
  const storeWithStatus = useYjsStore(roomId)

  return (
    <div style={{ height: '100vh' }}>
      <Tldraw store={storeWithStatus} />
    </div>
  )
}

Make Real - AI integration

How Make Real works

Code
TEXT
┌─────────────────────────────────────────────────────────┐
│  MAKE REAL WORKFLOW                                     │
├─────────────────────────────────────────────────────────┤
│  1. Draw a wireframe on the whiteboard                  │
│     ┌─────────────────────┐                             │
│     │  [Logo]   Nav  Nav  │                             │
│     │                     │                             │
│     │   Hero Section      │                             │
│     │   [ Button ]        │                             │
│     │                     │                             │
│     └─────────────────────┘                             │
│                                                         │
│  2. Select the elements                                 │
│                                                         │
│  3. Click "Make Real"                                   │
│                                                         │
│  4. AI generates HTML/CSS/JS                            │
│     ```html                                             │
│     <nav class="flex justify-between p-4">              │
│       <div class="logo">Logo</div>                      │
│       <div class="nav-links">...</div>                  │
│     </nav>                                              │
│     ...                                                 │
│     ```                                                 │
│                                                         │
│  5. Preview in the browser!                             │
└─────────────────────────────────────────────────────────┘

Implementing Make Real

Code
TypeScript
// Custom Make Real implementation
import { Tldraw, Editor, TLShapeId } from 'tldraw'
import Anthropic from '@anthropic-ai/sdk'

const anthropic = new Anthropic()

async function makeReal(editor: Editor, selectedIds: TLShapeId[]) {
  // 1. Export the selection as an image
  const blob = await editor.exportShapes(selectedIds, {
    type: 'png',
    scale: 2,
  })

  // 2. Convert to base64
  const base64 = await blobToBase64(blob)

  // 3. Send to Claude with a prompt
  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-5-20241022',
    max_tokens: 4096,
    messages: [{
      role: 'user',
      content: [
        {
          type: 'image',
          source: {
            type: 'base64',
            media_type: 'image/png',
            data: base64
          }
        },
        {
          type: 'text',
          text: `Transform this wireframe into HTML code with Tailwind CSS.
                 Return only the HTML code, no explanations.
                 The code should be responsive and semantic.`
        }
      ]
    }]
  })

  // 4. Extract HTML from the response
  const html = extractCode(response.content[0].text)

  // 5. Display the preview
  openPreview(html)
}

// Add a button to tldraw
const uiOverrides = {
  actions: (editor: Editor, actions: any) => ({
    ...actions,
    'make-real': {
      id: 'make-real',
      label: 'Make Real',
      kbd: 'shift+m',
      onSelect: () => {
        const selectedIds = editor.getSelectedShapeIds()
        if (selectedIds.length > 0) {
          makeReal(editor, selectedIds)
        }
      }
    }
  })
}

Self-hosting

Docker

docker-compose.yml
YAML
# docker-compose.yml
version: '3.8'

services:
  tldraw:
    image: tldraw/tldraw:latest
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    volumes:
      - tldraw_data:/app/data

volumes:
  tldraw_data:
Code
Bash
# Launch
docker-compose up -d

# Access
# http://localhost:3000

Custom instance with Next.js

Code
Bash
# Create a Next.js project with tldraw
npx create-next-app@latest my-tldraw --typescript
cd my-tldraw
npm install tldraw
TSapp/page.tsx
TypeScript
// app/page.tsx
'use client'

import dynamic from 'next/dynamic'

const Tldraw = dynamic(
  async () => (await import('tldraw')).Tldraw,
  { ssr: false }
)

import 'tldraw/tldraw.css'

export default function Home() {
  return (
    <div style={{ position: 'fixed', inset: 0 }}>
      <Tldraw />
    </div>
  )
}

Backend with Cloudflare Workers

TSworkers/tldraw-sync.ts
TypeScript
// workers/tldraw-sync.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('*', cors())

const documents = new Map<string, any>()

app.get('/room/:id', async (c) => {
  const id = c.req.param('id')
  const doc = documents.get(id) || { shapes: [], bindings: [] }
  return c.json(doc)
})

app.put('/room/:id', async (c) => {
  const id = c.req.param('id')
  const data = await c.req.json()
  documents.set(id, data)
  return c.json({ success: true })
})

export default app

Export and import

Programmatic export

Code
TypeScript
import { Tldraw, Editor } from 'tldraw'

function ExportButtons({ editor }: { editor: Editor }) {

  const exportPng = async () => {
    const shapeIds = editor.getCurrentPageShapeIds()
    const blob = await editor.exportShapes([...shapeIds], {
      type: 'png',
      scale: 2,
      background: true
    })

    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'whiteboard.png'
    a.click()
  }

  const exportSvg = async () => {
    const shapeIds = editor.getCurrentPageShapeIds()
    const svg = await editor.exportShapesSvg([...shapeIds], {
      background: true
    })

    const blob = new Blob([svg.innerHTML], { type: 'image/svg+xml' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'whiteboard.svg'
    a.click()
  }

  const exportJson = () => {
    const snapshot = editor.store.getSnapshot()
    const json = JSON.stringify(snapshot, null, 2)

    const blob = new Blob([json], { type: 'application/json' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = 'whiteboard.json'
    a.click()
  }

  return (
    <div className="export-buttons">
      <button onClick={exportPng}>Export PNG</button>
      <button onClick={exportSvg}>Export SVG</button>
      <button onClick={exportJson}>Export JSON</button>
    </div>
  )
}

Import from JSON

Code
TypeScript
function ImportButton({ editor }: { editor: Editor }) {
  const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return

    const text = await file.text()
    const snapshot = JSON.parse(text)

    editor.store.loadSnapshot(snapshot)
  }

  return (
    <input
      type="file"
      accept=".json"
      onChange={handleImport}
    />
  )
}

Use cases

Technical documentation

Code
TypeScript
// Embed in documentation (e.g. Docusaurus)
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

export function DiagramEmbed({ snapshotUrl }: { snapshotUrl: string }) {
  const [snapshot, setSnapshot] = useState(null)

  useEffect(() => {
    fetch(snapshotUrl)
      .then(r => r.json())
      .then(setSnapshot)
  }, [snapshotUrl])

  if (!snapshot) return <div>Loading...</div>

  return (
    <div style={{ height: 400, border: '1px solid #ccc' }}>
      <Tldraw
        snapshot={snapshot}
        hideUi
      />
    </div>
  )
}

Team brainstorming

Code
TypeScript
// Brainstorming room
export function BrainstormRoom({ teamId }: { teamId: string }) {
  const store = useSyncDemo({
    roomId: `brainstorm-${teamId}`,
    userPreferences: {
      name: currentUser.name,
      color: currentUser.color
    }
  })

  return (
    <div className="h-screen">
      <header className="p-4 bg-gray-100 flex justify-between">
        <h1>Team Brainstorm</h1>
        <ParticipantsList />
      </header>
      <div className="h-[calc(100vh-60px)]">
        <Tldraw store={store} />
      </div>
    </div>
  )
}

Online education

Code
TypeScript
// Teacher whiteboard with recording capability
export function TeacherWhiteboard() {
  const editorRef = useRef<Editor | null>(null)
  const [isRecording, setIsRecording] = useState(false)
  const [history, setHistory] = useState<any[]>([])

  const startRecording = () => {
    setIsRecording(true)
    editorRef.current?.store.listen((update) => {
      setHistory(h => [...h, { ...update, timestamp: Date.now() }])
    })
  }

  const playback = () => {
    history.forEach((update, i) => {
      setTimeout(() => {
        editorRef.current?.store.applyDiff(update)
      }, update.timestamp - history[0].timestamp)
    })
  }

  return (
    <div>
      <div className="controls">
        <button onClick={startRecording}>
          {isRecording ? '⏹ Stop' : '⏺ Record'}
        </button>
        <button onClick={playback}>▶ Playback</button>
      </div>
      <Tldraw
        onMount={(editor) => { editorRef.current = editor }}
      />
    </div>
  )
}

Pricing

PlanPriceIncludes
Open Source$0Full library, self-hosting, unlimited
tldraw.com$0Free online whiteboard, multiplayer
EnterpriseContactDedicated support, SLA, custom features

tldraw is 100% free under the Apache 2.0 license.

FAQ - Frequently asked questions

Can I use tldraw commercially?

Yes, the Apache 2.0 license allows commercial use without restrictions.

How do I save the whiteboard state?

Use editor.store.getSnapshot() to export and editor.store.loadSnapshot() to import. You can save to localStorage, a database, or send it to a server.

Does tldraw work offline?

Yes, the core functionality works offline. For multiplayer you need a connection.

How do I add custom shapes?

Create a class extending BaseBoxShapeUtil or BaseShapeUtil, define type, props, component, and indicator, then pass it to shapeUtils.

Does tldraw support touch/stylus?

Yes, tldraw has full support for touch, stylus, and pressure sensitivity.

How do I integrate with a backend?

Use the official @tldraw/sync or implement your own synchronization with Yjs, WebSocket, or REST API.

Can I hide the UI?

Yes, use the hideUi prop or the components prop to selectively hide elements.

Summary

tldraw is a powerful, free visualization tool:

  • 100% open-source - Apache 2.0, no hidden costs
  • Infinite canvas - Unlimited drawing space
  • React library - Easy integration with applications
  • Real-time multiplayer - Collaboration in real time
  • Make Real - AI generates code from wireframes
  • Customizable - Custom tools, shapes, UI
  • Self-hostable - Full control over your data

tldraw is the perfect choice for teams looking for a free, extensible whiteboard for diagrams, wireframes, and collaboration.