Skip to main content

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.

The canvas is where shapes are rendered and user interactions occur. tldraw’s canvas system combines HTML and SVG rendering with a reactive camera system to deliver smooth, performant infinite canvas experiences.

Overview

The canvas system consists of:
  • Camera: Controls viewport position and zoom
  • Rendering layers: Separate HTML and SVG layers for optimal performance
  • Shape culling: Only renders visible shapes
  • Transform system: Converts between coordinate spaces
  • Performance optimizations: Level-of-detail, throttling, and batching

Canvas architecture

Rendering layers

The canvas uses multiple layers:
<TldrawEditor>
  <div className="tl-canvas">
    {/* Background layer */}
    <Background />
    
    {/* SVG layer for vector shapes */}
    <svg className="tl-svg-layer">
      <g className="tl-shapes">
        <Shape shapeId={id1} />
        <Shape shapeId={id2} />
      </g>
      <g className="tl-indicators">
        {/* Selection indicators */}
      </g>
    </svg>
    
    {/* HTML layer for rich content */}
    <div className="tl-html-layer">
      <Shape shapeId={id3} /> {/* e.g., text with input */}
      <Shape shapeId={id4} /> {/* e.g., video embeds */}
    </div>
    
    {/* UI overlay layer */}
    <div className="tl-overlay">
      {/* Handles, toolbar, etc. */}
    </div>
  </div>
</TldrawEditor>

Camera system

The camera defines what portion of the infinite canvas is visible:
interface TLCamera {
  x: number  // Pan X
  y: number  // Pan Y
  z: number  // Zoom level (1 = 100%)
}

// Get current camera
const camera = editor.getCamera()
// { x: 0, y: 0, z: 1 }

// Set camera
editor.setCamera({ x: 100, y: 100, z: 1.5 })

// Animate camera
editor.animateCamera(
  { x: 500, y: 500, z: 2 },
  { duration: 500, easing: 'easeInOutCubic' }
)

Coordinate systems

tldraw uses multiple coordinate spaces:

Screen space

Pixels relative to the viewport:
// Mouse position in screen space
const screenPoint = { x: 150, y: 200 }

// Convert to page space
const pagePoint = editor.screenToPage(screenPoint)

Page space

Coordinates on the infinite canvas:
// Shape position in page space
const shape = editor.createShape({
  type: 'geo',
  x: 500,    // Page X
  y: 300,    // Page Y
  props: { w: 100, h: 100 }
})

// Convert to screen space
const screenPoint = editor.pageToScreen({ x: 500, y: 300 })

Shape space

Coordinates relative to a shape’s origin:
// Point in shape's local coordinates
const localPoint = editor.getPointInShapeSpace(
  shape,
  pagePoint
)

// Transform from shape to page space
const pageTransform = editor.getShapePageTransform(shape)
const pagePoint = Mat.applyToPoint(pageTransform, localPoint)

Rendering shapes

SVG rendering

For vector graphics, use SVGContainer:
import { SVGContainer } from '@tldraw/editor'

component(shape: RectShape) {
  return (
    <SVGContainer>
      <rect
        width={shape.props.w}
        height={shape.props.h}
        fill={shape.props.color}
        stroke="black"
        strokeWidth={2}
      />
    </SVGContainer>
  )
}

HTML rendering

For rich content, use HTMLContainer:
import { HTMLContainer } from '@tldraw/editor'

component(shape: TextShape) {
  return (
    <HTMLContainer>
      <div
        style={{
          width: shape.props.w,
          height: shape.props.h,
          fontSize: shape.props.fontSize,
          color: shape.props.color
        }}
      >
        {shape.props.text}
      </div>
    </HTMLContainer>
  )
}

Hybrid rendering

Combine SVG and HTML for complex shapes:
component(shape: CardShape) {
  return (
    <>
      {/* SVG for the border */}
      <SVGContainer>
        <rect
          width={shape.props.w}
          height={shape.props.h}
          fill="none"
          stroke="black"
          strokeWidth={2}
          rx={8}
        />
      </SVGContainer>
      
      {/* HTML for the content */}
      <HTMLContainer>
        <div className="card-content">
          <h3>{shape.props.title}</h3>
          <p>{shape.props.description}</p>
        </div>
      </HTMLContainer>
    </>
  )
}

Performance optimizations

Shape culling

Only visible shapes are rendered:
import { useShapeCulling } from '@tldraw/editor'

function ShapeRenderer() {
  const editor = useEditor()
  
  // Get only visible shapes
  const visibleShapes = useShapeCulling(editor)
  
  return (
    <>
      {visibleShapes.map(shape => (
        <Shape key={shape.id} shape={shape} />
      ))}
    </>
  )
}

Level-of-detail (LOD)

Simplify rendering at low zoom levels:
import { useValue } from '@tldraw/state-react'

component(shape: ComplexShape) {
  const editor = this.editor
  const zoom = useValue('zoom', () => editor.getZoomLevel(), [editor])
  
  // Simplified at low zoom
  if (zoom < 0.15) {
    return (
      <SVGContainer>
        <rect
          width={shape.props.w}
          height={shape.props.h}
          fill={shape.props.color}
        />
      </SVGContainer>
    )
  }
  
  // Full detail at normal zoom
  return <DetailedShapeComponent shape={shape} />
}

Text outline LOD

tldraw automatically disables text outlines at low zoom:
// This is handled automatically by the editor
// Text outlines are disabled when zoom < textShadowLod option

const editor = new Editor({
  // ...
  options: {
    textShadowLod: 0.35  // Default: disable outlines below 35% zoom
  }
})

Throttle expensive operations

import { throttleToNextFrame } from '@tldraw/utils'

class MyShapeUtil extends ShapeUtil<MyShape> {
  private updateThrottled = throttleToNextFrame(() => {
    // Expensive operation
    this.computeComplexGeometry()
  })
  
  onResize(shape: MyShape, info: TLResizeInfo) {
    // Throttle to animation frame
    this.updateThrottled()
    
    return shape
  }
}

Camera controls

Zoom operations

// Get current zoom
const zoom = editor.getZoomLevel()

// Set zoom
editor.setCamera({ ...editor.getCamera(), z: 1.5 })

// Zoom in/out
editor.zoomIn()
editor.zoomOut()

// Reset zoom
editor.resetZoom()

// Zoom to fit content
editor.zoomToFit()

// Zoom to selection
editor.zoomToSelection()

// Zoom to specific bounds
editor.zoomToBounds(bounds, { 
  duration: 300,
  easing: 'easeInOutCubic' 
})

Pan operations

// Pan to point
editor.centerOnPoint({ x: 500, y: 500 })

// Pan by delta
const camera = editor.getCamera()
editor.setCamera({
  x: camera.x + 100,
  y: camera.y + 50,
  z: camera.z
})

// Animate pan
editor.animateCamera(
  { x: 1000, y: 1000, z: 1 },
  { duration: 500 }
)

Camera constraints

Constrain camera movement and zoom:
const editor = new Editor({
  // ...
  options: {
    camera: {
      zoomMin: 0.1,
      zoomMax: 8,
      zoomSpeed: 1,
      isLocked: false,
      panSpeed: 1,
      wheelBehavior: 'zoom'  // or 'pan'
    }
  }
})

// Lock camera
editor.updateInstanceState({ 
  canMoveCamera: false 
})

// Unlock camera
editor.updateInstanceState({ 
  canMoveCamera: true 
})

Viewport and bounds

Get viewport bounds

// Viewport bounds in page space
const viewport = editor.getViewportPageBounds()
// Box { x, y, w, h, minX, minY, maxX, maxY }

// Viewport center
const center = viewport.center

// Check if shape is in viewport
const shapeInView = viewport.includes(editor.getShapePageBounds(shape))

Screen bounds

// Size of the canvas element
const screenBounds = editor.getViewportScreenBounds()
// { x: 0, y: 0, w: 1920, h: 1080 }

Custom rendering

Background

Customize the canvas background:
import { TLEditorComponents } from '@tldraw/editor'

function CustomBackground() {
  return (
    <svg className="tl-background">
      <defs>
        <pattern
          id="grid"
          width={20}
          height={20}
          patternUnits="userSpaceOnUse"
        >
          <rect width={20} height={20} fill="white" />
          <path
            d="M 20 0 L 0 0 0 20"
            fill="none"
            stroke="#e0e0e0"
            strokeWidth={1}
          />
        </pattern>
      </defs>
      <rect width="100%" height="100%" fill="url(#grid)" />
    </svg>
  )
}

const components: TLEditorComponents = {
  Background: CustomBackground
}

<Tldraw components={components} />

Overlays

Add custom UI overlays:
function CustomOverlay() {
  const editor = useEditor()
  const camera = useValue('camera', () => editor.getCamera(), [editor])
  
  return (
    <div className="custom-overlay">
      <div>Zoom: {Math.round(camera.z * 100)}%</div>
      <div>Position: ({Math.round(camera.x)}, {Math.round(camera.y)})</div>
    </div>
  )
}

const components: TLEditorComponents = {
  InFrontOfTheCanvas: CustomOverlay
}

Shape indicators

Customize selection indicators:
indicator(shape: MyShape) {
  const bounds = this.editor.getShapeGeometry(shape).bounds
  
  return (
    <>
      {/* Selection outline */}
      <rect
        x={bounds.x}
        y={bounds.y}
        width={bounds.width}
        height={bounds.height}
        fill="none"
        stroke="blue"
        strokeWidth={2}
      />
      
      {/* Custom indicator elements */}
      <circle
        cx={bounds.center.x}
        cy={bounds.center.y}
        r={5}
        fill="blue"
      />
    </>
  )
}

Canvas events

The canvas handles various input events:
import { useCanvasEvents } from '@tldraw/editor'

function Canvas() {
  const events = useCanvasEvents()
  
  return (
    <div
      className="tl-canvas"
      {...events}
    >
      {/* Canvas content */}
    </div>
  )
}

// Events handled:
// - onPointerDown
// - onPointerMove
// - onPointerUp
// - onWheel
// - onKeyDown
// - onKeyUp
// - etc.

Exporting canvas content

Export to SVG

import { exportToSvg } from '@tldraw/editor'

// Export selected shapes
const svg = await editor.getSvg(
  editor.getSelectedShapes(),
  { background: true }
)

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

Export to image

// Export as PNG
const blob = await editor.exportToBlob({
  format: 'png',
  ids: editor.getSelectedShapeIds(),
  background: true,
  scale: 2  // 2x resolution
})

if (blob) {
  const url = URL.createObjectURL(blob)
  // Use the image URL
}

Best practices

Use culling: Let the editor handle shape culling automatically. Don’t try to manually filter visible shapes unless you have a specific reason.
Implement LOD: For complex shapes, implement level-of-detail rendering to maintain performance at low zoom levels.
Choose the right container: Use SVGContainer for vector graphics and HTMLContainer for rich content. Avoid mixing unless necessary.
Optimize expensive rendering: Use useMemo, useValue, and throttling for expensive shape rendering operations.
  • Editor - Working with the Editor API
  • Shapes - Rendering custom shapes
  • Camera API - Complete camera API reference