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.

You can create custom shapes in tldraw by creating a shape util and passing it to the Tldraw component. Shape utils define how shapes behave and render.

Basic custom shape

CustomShapeExample.tsx
import {
  Geometry2d,
  HTMLContainer,
  RecordProps,
  Rectangle2d,
  ShapeUtil,
  T,
  TLResizeInfo,
  TLShape,
  Tldraw,
  resizeBox,
} from 'tldraw'
import 'tldraw/tldraw.css'

const MY_CUSTOM_SHAPE_TYPE = 'my-custom-shape'

// [1] Extend the global shape props map
declare module 'tldraw' {
  export interface TLGlobalShapePropsMap {
    [MY_CUSTOM_SHAPE_TYPE]: { w: number; h: number; text: string }
  }
}

// [2] Define the shape type
type ICustomShape = TLShape<typeof MY_CUSTOM_SHAPE_TYPE>

// [3] Create the shape util
export class MyShapeUtil extends ShapeUtil<ICustomShape> {
  static override type = MY_CUSTOM_SHAPE_TYPE
  static override props: RecordProps<ICustomShape> = {
    w: T.number,
    h: T.number,
    text: T.string,
  }

  // Define default props
  getDefaultProps(): ICustomShape['props'] {
    return {
      w: 200,
      h: 200,
      text: "I'm a shape!",
    }
  }

  // Control shape behavior
  override canEdit() {
    return false
  }
  override canResize() {
    return true
  }
  override isAspectRatioLocked() {
    return false
  }

  // Define geometry for hit-testing and bindings
  getGeometry(shape: ICustomShape): Geometry2d {
    return new Rectangle2d({
      width: shape.props.w,
      height: shape.props.h,
      isFilled: true,
    })
  }

  // Handle resizing
  override onResize(shape: any, info: TLResizeInfo<any>) {
    return resizeBox(shape, info)
  }

  // Render the shape
  component(shape: ICustomShape) {
    return (
      <HTMLContainer style={{ backgroundColor: '#efefef' }}>
        {shape.props.text}
      </HTMLContainer>
    )
  }

  // Render the selection indicator
  indicator(shape: ICustomShape) {
    return <rect width={shape.props.w} height={shape.props.h} />
  }
}

// [4] Pass to Tldraw component
const customShape = [MyShapeUtil]

export default function CustomShapeExample() {
  return (
    <div className="tldraw__editor">
      <Tldraw
        shapeUtils={customShape}
        onMount={(editor) => {
          editor.createShape({ type: MY_CUSTOM_SHAPE_TYPE, x: 100, y: 100 })
        }}
      />
    </div>
  )
}

Key concepts

1. Extend TLGlobalShapePropsMap

First, extend the global type system to include your shape’s properties:
declare module 'tldraw' {
  export interface TLGlobalShapePropsMap {
    'my-custom-shape': { w: number; h: number; text: string }
  }
}

2. Define shape type

Create a type for your shape using TLShape:
type ICustomShape = TLShape<'my-custom-shape'>

3. Create ShapeUtil class

Extend ShapeUtil and implement required methods:
static override type = 'my-custom-shape'
static override props: RecordProps<ICustomShape> = {
  w: T.number,
  h: T.number,
  text: T.string,
}

Using BaseBoxShapeUtil

For box-shaped shapes, extend BaseBoxShapeUtil to get default implementations:
import { BaseBoxShapeUtil } from 'tldraw'

class MyBoxShape extends BaseBoxShapeUtil<ICustomShape> {
  static override type = 'my-box-shape'
  static override props = {
    w: T.number,
    h: T.number,
  }

  getDefaultProps(): ICustomShape['props'] {
    return { w: 200, h: 200 }
  }

  component(shape: ICustomShape) {
    return <HTMLContainer>Box content</HTMLContainer>
  }

  indicator(shape: ICustomShape) {
    return <rect width={shape.props.w} height={shape.props.h} />
  }
}
When you extend BaseBoxShapeUtil, you don’t need to define getGeometry or onResize - they’re provided automatically.

Shape methods

Behavior methods

canEdit(): boolean  // Whether shape can enter edit mode
canResize(): boolean  // Whether shape can be resized
canBind(): boolean  // Whether arrows can bind to this shape
isAspectRatioLocked(): boolean  // Lock aspect ratio when resizing

Rendering methods

component(shape): JSX.Element  // Main shape rendering
indicator(shape): JSX.Element  // Selection outline

Event handlers

onResize(shape, info): TLShape  // Handle resize events
onDoubleClick(shape): void  // Handle double-clicks
onEditEnd(shape): void  // Handle edit mode exit

Validators

Use tldraw’s validator library to define type-safe props:
import { T } from 'tldraw'

static override props = {
  w: T.number,
  h: T.number,
  text: T.string,
  color: T.string,
  opacity: T.number,
}
The validator ensures the store always has valid shape data.