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.

Tldraw provides flexible rendering options for shapes through the component() and indicator() methods. Shapes can use SVG for vector graphics, HTML for text and interactive elements, or a mix of both.

Component rendering

The component() method returns a React element that renders the shape’s visual appearance:
component(shape: MyShape) {
  return (
    <SVGContainer>
      <rect
        width={shape.props.w}
        height={shape.props.h}
        fill="blue"
        stroke="black"
        strokeWidth={2}
      />
    </SVGContainer>
  )
}

Container types

Tldraw provides two container components for shape rendering:

SVGContainer

For SVG content that needs proper positioning and transformation:
import { SVGContainer } from 'tldraw'

component(shape: MyShape) {
  return (
    <SVGContainer>
      <circle cx={50} cy={50} r={40} fill="red" />
      <path d="M 10 10 L 90 90" stroke="black" />
    </SVGContainer>
  )
}
id
string
Optional ID for the SVG container
style
CSSProperties
Additional CSS styles (use for min-width/min-height)

HTMLContainer

For HTML content like text, forms, or interactive elements:
import { HTMLContainer } from 'tldraw'

component(shape: MyShape) {
  return (
    <HTMLContainer
      style={{
        width: shape.props.w,
        height: shape.props.h,
        overflow: 'hidden',
      }}
    >
      <div className="my-shape-content">
        <h1>Hello World</h1>
        <p>HTML content here</p>
      </div>
    </HTMLContainer>
  )
}
Both containers automatically handle positioning, rotation, and z-index. You don’t need to apply transforms manually.

Combining SVG and HTML

Shapes often combine both for maximum flexibility:
component(shape: TLGeoShape) {
  const theme = useDefaultColorTheme()
  
  return (
    <>
      <SVGContainer>
        <GeoShapeBody shape={shape} />
      </SVGContainer>
      <HTMLContainer
        style={{
          width: shape.props.w,
          height: shape.props.h,
          overflow: 'hidden',
        }}
      >
        <RichTextLabel
          shapeId={shape.id}
          font={shape.props.font}
          fontSize={LABEL_FONT_SIZES[shape.props.size]}
          richText={shape.props.richText}
          labelColor={getColorValue(theme, shape.props.labelColor, 'solid')}
        />
      </HTMLContainer>
    </>
  )
}

Indicators

Indicators show when a shape is selected or hovered. They should be simple, non-interactive SVG elements:

SVG indicators (legacy)

indicator(shape: MyShape) {
  return (
    <rect
      width={shape.props.w}
      height={shape.props.h}
      fill="none"
      stroke="var(--color-selected)"
      strokeWidth={1}
    />
  )
}
For better performance, use canvas-based indicators:
override useLegacyIndicator() {
  return false  // Use canvas rendering
}

override getIndicatorPath(shape: MyShape): Path2D | undefined {
  const path = new Path2D()
  path.rect(0, 0, shape.props.w, shape.props.h)
  return path
}
Canvas indicators are 2-3x faster than SVG indicators for complex shapes.

Complex indicators with clipping

For shapes that need clipping (like arrows with labels):
override getIndicatorPath(shape: TLArrowShape): TLIndicatorPath {
  const bodyPath = new Path2D()
  // ... build arrow body path
  
  const labelGeometry = this.editor.getShapeGeometry(shape).children[1]
  if (!labelGeometry) return bodyPath
  
  // Create clip path
  const clipPath = new Path2D()
  
  // Outer rectangle (what to keep)
  clipPath.rect(
    bounds.minX - 100,
    bounds.minY - 100,
    bounds.width + 200,
    bounds.height + 200
  )
  
  // Label cutout (what to remove)
  const lb = labelGeometry.bounds
  clipPath.roundRect(lb.x, lb.y, lb.w, lb.h, 3.5)
  
  return {
    path: bodyPath,
    clipPath: clipPath,
    additionalPaths: [arrowheadPath],
  }
}
When using clipPath, create the outer rectangle clockwise and cutouts counter-clockwise for proper winding order.

Theming and colors

Using color themes

import { useDefaultColorTheme, getColorValue } from 'tldraw'

component(shape: MyShape) {
  const theme = useDefaultColorTheme()
  const fillColor = getColorValue(theme, shape.props.color, 'solid')
  const strokeColor = getColorValue(theme, shape.props.color, 'outline')
  
  return (
    <SVGContainer>
      <rect
        width={shape.props.w}
        height={shape.props.h}
        fill={fillColor}
        stroke={strokeColor}
      />
    </SVGContainer>
  )
}

Color variants

Full opacity color for fills and strokes
const color = getColorValue(theme, 'blue', 'solid')

Fill patterns

Tldraw provides built-in fill patterns:
import { ShapeFill, getFillDefForCanvas } from 'tldraw'

component(shape: MyShape) {
  const theme = useDefaultColorTheme()
  
  return (
    <SVGContainer>
      {shape.props.fill !== 'none' && (
        <ShapeFill
          theme={theme}
          d={pathData}
          color={shape.props.color}
          fill={shape.props.fill}
          scale={shape.props.scale}
        />
      )}
      <path
        d={pathData}
        fill="none"
        stroke={getColorValue(theme, shape.props.color, 'solid')}
        strokeWidth={2}
      />
    </SVGContainer>
  )
}

getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
  return [getFillDefForCanvas()]
}
Register canvas SVG defs with getCanvasSvgDefs() to ensure patterns are available in the SVG context.

Dash patterns

Handle different stroke styles:
const DASH_PATTERNS = {
  draw: 'freehand',    // Hand-drawn appearance
  solid: 'none',       // Solid line
  dashed: '8 8',       // Dashed
  dotted: '2 6',       // Dotted
}

component(shape: MyShape) {
  const strokeDasharray = DASH_PATTERNS[shape.props.dash]
  
  return (
    <SVGContainer>
      <path
        d={pathData}
        stroke="black"
        strokeWidth={2}
        strokeDasharray={strokeDasharray}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </SVGContainer>
  )
}

Font rendering

Registering fonts

Ensure fonts are loaded before rendering:
import { getFontsFromRichText } from 'tldraw'

override getFontFaces(shape: MyShape) {
  if (isEmptyRichText(shape.props.richText)) {
    return EMPTY_ARRAY
  }
  
  return getFontsFromRichText(this.editor, shape.props.richText, {
    family: `tldraw_${shape.props.font}`,
    weight: 'normal',
    style: 'normal',
  })
}

Rendering text

Use the RichTextLabel component for consistent text rendering:
import { RichTextLabel, FONT_SIZES, TEXT_PROPS } from 'tldraw'

component(shape: TLTextShape) {
  const theme = useDefaultColorTheme()
  
  return (
    <RichTextLabel
      shapeId={shape.id}
      type="text"
      font={shape.props.font}
      fontSize={FONT_SIZES[shape.props.size]}
      lineHeight={TEXT_PROPS.lineHeight}
      align={shape.props.textAlign}
      verticalAlign="middle"
      richText={shape.props.richText}
      labelColor={getColorValue(theme, shape.props.color, 'solid')}
      isSelected={isSelected}
      wrap
      showTextOutline={true}
    />
  )
}

SVG export

Provide a clean SVG representation for exports:
toSvg(shape: MyShape, ctx: SvgExportContext): ReactElement | null {
  // Register required definitions
  ctx.addExportDef(getFillDefForExport(shape.props.fill))
  
  const theme = getDefaultColorTheme(ctx)
  
  return (
    <g>
      <rect
        width={shape.props.w}
        height={shape.props.h}
        fill={getColorValue(theme, shape.props.color, 'solid')}
        stroke="black"
        strokeWidth={2}
      />
      <RichTextSVG
        fontSize={FONT_SIZES[shape.props.size]}
        font={shape.props.font}
        richText={shape.props.richText}
        labelColor={getColorValue(theme, shape.props.color, 'solid')}
        bounds={textBounds}
      />
    </g>
  )
}

Export definitions

Register reusable SVG elements:
getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
  return [
    getFillDefForCanvas(),
    {
      key: 'my-gradient',
      component: MyGradientDef,
    },
    {
      key: 'my-marker',
      component: MyMarkerDef,
    },
  ]
}

function MyGradientDef() {
  return (
    <linearGradient id="my-gradient">
      <stop offset="0%" stopColor="red" />
      <stop offset="100%" stopColor="blue" />
    </linearGradient>
  )
}

Performance optimizations

Zoom-based rendering

Simplify rendering at low zoom levels:
import { useEfficientZoomThreshold } from 'tldraw'

component(shape: MyShape) {
  const isForceSolid = useEfficientZoomThreshold(0.25 / shape.props.scale)
  
  return (
    <SVGContainer>
      <path
        d={pathData}
        stroke="black"
        strokeDasharray={isForceSolid ? 'none' : '8 8'}
      />
    </SVGContainer>
  )
}

Culling

Control whether shapes can be culled when offscreen:
override canCull(shape: MyShape): boolean {
  return true  // Allow culling (default)
}
Culled shapes have display: none applied but remain in the DOM.

Conditional rendering

Only render expensive elements when needed:
component(shape: MyShape) {
  const isEditing = this.editor.getEditingShapeId() === shape.id
  const isEmpty = isEmptyRichText(shape.props.richText)
  const showLabel = isEditing || !isEmpty
  
  return (
    <>
      <SVGContainer>
        <rect width={shape.props.w} height={shape.props.h} />
      </SVGContainer>
      {showLabel && (
        <HTMLContainer>
          <RichTextLabel {...labelProps} />
        </HTMLContainer>
      )}
    </>
  )
}

Memoization

Use React hooks to avoid unnecessary recalculations:
import { useMemo } from 'react'

component(shape: MyShape) {
  const pathData = useMemo(() => {
    return buildComplexPath(shape.props)
  }, [shape.props.w, shape.props.h, shape.props.points])
  
  return (
    <SVGContainer>
      <path d={pathData} />
    </SVGContainer>
  )
}

Background rendering

For shapes that need to render content behind other shapes:
override providesBackgroundForChildren(shape: MyShape): boolean {
  return true  // This shape provides a background layer
}

backgroundComponent(shape: MyShape) {
  return (
    <SVGContainer>
      <rect
        width={shape.props.w}
        height={shape.props.h}
        fill="white"
        opacity={0.8}
      />
    </SVGContainer>
  )
}

Clipping child shapes

Define a clip path for child shapes:
getClipPath(shape: MyFrameShape): Vec[] | undefined {
  const strokeWidth = 2
  const inset = strokeWidth / 2
  
  return [
    new Vec(inset, inset),
    new Vec(shape.props.w - inset, inset),
    new Vec(shape.props.w - inset, shape.props.h - inset),
    new Vec(inset, shape.props.h - inset),
  ]
}

shouldClipChild(child: TLShape): boolean {
  // Only clip certain child types
  return child.type !== 'text'
}
The clip path should define the inner boundary. Inset by half the stroke width to clip to the inside edge.

Selection bounds customization

override hideSelectionBoundsBg(shape: MyShape): boolean {
  return false  // Show background of selection bounds
}

override hideSelectionBoundsFg(shape: MyShape): boolean {
  return false  // Show foreground of selection bounds
}

override expandSelectionOutlinePx(shape: MyShape): number | Box {
  return 5  // Expand outline by 5px
}

Next steps

Shape interactions

Handle user interactions and events

ShapeUtil API

Complete ShapeUtil method reference