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.
Shapes are the visual elements users create and interact with on the canvas. Each shape type (rectangle, arrow, text, etc.) is defined by a ShapeUtil class that controls its behavior, geometry, and rendering.
Overview
The shape system in tldraw is highly extensible:
- Shape records: Data stored in the reactive store (position, size, props)
- ShapeUtil classes: Define behavior for each shape type
- Geometry system: Calculate bounds, hitboxes, and collision detection
- Rendering: HTML and SVG components for display
- Migrations: Handle schema changes over time
Anatomy of a shape
Every shape has two parts:
1. Shape record (data)
Stored in the Editor’s store:
interface TLGeoShape extends TLBaseShape<'geo', TLGeoShapeProps> {
type: 'geo'
props: {
w: number
h: number
geo: 'rectangle' | 'ellipse' | 'triangle' | ...
color: string
fill: string
// ... other props
}
}
2. ShapeUtil class (behavior)
Defines how the shape works:
import { BaseBoxShapeUtil, TLGeoShape } from '@tldraw/editor'
export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
static override type = 'geo' as const
static override props = geoShapeProps
getDefaultProps(): TLGeoShape['props'] {
return {
w: 100,
h: 100,
geo: 'rectangle',
color: 'black',
fill: 'none'
}
}
getGeometry(shape: TLGeoShape) {
// Return geometry for bounds, hit testing, etc.
}
component(shape: TLGeoShape) {
// Return JSX to render the shape
}
indicator(shape: TLGeoShape) {
// Return SVG for selection indicator
}
}
Creating a custom shape
Here’s a complete example of a custom star shape:
Define your shape’s props and TypeScript types:
import { TLBaseShape, T } from '@tldraw/editor'
type StarShape = TLBaseShape<
'star',
{
w: number
h: number
points: number
color: string
}
>
const starShapeProps = {
w: T.number,
h: T.number,
points: T.number,
color: T.string
}
Create the ShapeUtil class
Implement the required methods:
import {
ShapeUtil,
Polygon2d,
SVGContainer,
HTMLContainer
} from '@tldraw/editor'
export class StarShapeUtil extends ShapeUtil<StarShape> {
static override type = 'star' as const
static override props = starShapeProps
getDefaultProps(): StarShape['props'] {
return {
w: 100,
h: 100,
points: 5,
color: 'black'
}
}
getGeometry(shape: StarShape) {
const { w, h, points } = shape.props
const vertices = []
// Calculate star vertices
for (let i = 0; i < points * 2; i++) {
const angle = (i * Math.PI) / points
const radius = i % 2 === 0 ? 0.5 : 0.2
vertices.push([
(Math.cos(angle) * radius + 0.5) * w,
(Math.sin(angle) * radius + 0.5) * h
])
}
return new Polygon2d({
points: vertices,
isFilled: true
})
}
component(shape: StarShape) {
const geometry = this.getGeometry(shape)
const pathData = 'M' + geometry.vertices.map(v => `${v.x},${v.y}`).join('L') + 'Z'
return (
<SVGContainer>
<path
d={pathData}
fill="none"
stroke={shape.props.color}
strokeWidth={2}
/>
</SVGContainer>
)
}
indicator(shape: StarShape) {
const geometry = this.getGeometry(shape)
const pathData = 'M' + geometry.vertices.map(v => `${v.x},${v.y}`).join('L') + 'Z'
return <path d={pathData} />
}
}
Add your ShapeUtil when creating the editor:
import { Tldraw } from 'tldraw'
import { StarShapeUtil } from './StarShapeUtil'
function App() {
return (
<Tldraw
shapeUtils={[StarShapeUtil]}
onMount={(editor) => {
// Create a star shape
editor.createShape({
type: 'star',
x: 100,
y: 100,
props: { w: 200, h: 200, points: 7 }
})
}}
/>
)
}
ShapeUtil methods
Required methods
These methods must be implemented:
class MyShapeUtil extends ShapeUtil<MyShape> {
// Default prop values
getDefaultProps(): MyShape['props']
// Geometry for bounds and hit testing
getGeometry(shape: MyShape): Geometry2d
// Render the shape (HTML or SVG)
component(shape: MyShape): React.ReactElement
// Selection indicator (SVG only)
indicator(shape: MyShape): React.ReactElement
}
Optional methods
Customize behavior with these optional methods:
class MyShapeUtil extends ShapeUtil<MyShape> {
// Can the shape be edited (e.g., text input)?
canEdit() { return true }
// Can the shape be resized?
canResize() { return true }
// Can the shape be cropped?
canCrop() { return false }
// Can bindings attach to this shape?
canBind({ bindingType, fromShape, toShape }) {
return bindingType === 'arrow'
}
// Handle resize
onResize(shape: MyShape, info: TLResizeInfo<MyShape>) {
return {
...shape,
props: {
w: info.newBounds.width,
h: info.newBounds.height
}
}
}
// Handle rotation
onRotate(shape: MyShape, rotation: number) {
// Custom rotation behavior
}
// Called when shape is double-clicked
onDoubleClick(shape: MyShape) {
this.editor.setEditingShape(shape.id)
}
// Called when editing starts
onEditEnd(shape: MyShape) {
// Cleanup after editing
}
}
Geometry system
The geometry system handles shape bounds, hit testing, and collisions:
Available geometry types
import {
Rectangle2d,
Polygon2d,
Ellipse2d,
Edge2d,
Polyline2d,
Group2d
} from '@tldraw/editor'
// Rectangle
getGeometry(shape: RectShape) {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true
})
}
// Ellipse
getGeometry(shape: EllipseShape) {
return new Ellipse2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true
})
}
// Polygon (for custom shapes)
getGeometry(shape: CustomShape) {
const vertices = calculateVertices(shape)
return new Polygon2d({
points: vertices,
isFilled: true
})
}
// Composite geometry
getGeometry(shape: ComplexShape) {
return new Group2d({
children: [
new Rectangle2d({ /* ... */ }),
new Ellipse2d({ /* ... */ })
]
})
}
Using geometry
The editor uses geometry for:
// Get shape bounds
const bounds = editor.getShapeGeometry(shape).bounds
// Hit testing
const isHit = editor.isPointInShape(
shape,
{ x: 100, y: 100 },
{ hitInside: true }
)
// Selection intersection
const intersects = editor.getShapeGeometry(shape)
.hitTestLineSegment(lineStart, lineEnd)
Rendering shapes
HTML rendering
Use HTMLContainer for HTML-based shapes:
import { HTMLContainer } from '@tldraw/editor'
component(shape: MyShape) {
return (
<HTMLContainer>
<div
style={{
width: shape.props.w,
height: shape.props.h,
background: shape.props.color,
borderRadius: '8px'
}}
>
{shape.props.text}
</div>
</HTMLContainer>
)
}
SVG rendering
Use SVGContainer for SVG-based shapes:
import { SVGContainer } from '@tldraw/editor'
component(shape: MyShape) {
return (
<SVGContainer>
<rect
width={shape.props.w}
height={shape.props.h}
fill={shape.props.color}
rx={8}
/>
</SVGContainer>
)
}
For complex shapes, use level-of-detail (LOD) rendering:
import { useValue } from '@tldraw/state-react'
component(shape: ComplexShape) {
const editor = this.editor
const zoom = useValue('zoom', () => editor.getZoomLevel(), [editor])
// Simplified rendering at low zoom
if (zoom < 0.2) {
return <SimplifiedComponent shape={shape} />
}
// Full detail at normal zoom
return <DetailedComponent shape={shape} />
}
Shape props and styles
Defining props
Use validators to define shape props:
import { T, StyleProp, DefaultColorStyle } from '@tldraw/editor'
const myShapeProps = {
w: T.number,
h: T.number,
color: DefaultColorStyle, // Shared style
text: T.string,
opacity: T.number
}
Style props
Style props are remembered between shapes and can be set on multiple shapes:
import { DefaultColorStyle, DefaultSizeStyle } from '@tldraw/editor'
static override props = {
color: DefaultColorStyle, // red, blue, green, etc.
size: DefaultSizeStyle, // s, m, l, xl
// ... other props
}
// Set style on multiple shapes
editor.setStyleForSelectedShapes(DefaultColorStyle, 'red')
editor.setStyleForSelectedShapes(DefaultSizeStyle, 'l')
BaseBoxShapeUtil
For rectangular shapes, extend BaseBoxShapeUtil for built-in resize handles:
import { BaseBoxShapeUtil } from '@tldraw/editor'
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
static override type = 'card'
getDefaultProps() {
return { w: 200, h: 100, title: '' }
}
getGeometry(shape: CardShape) {
return new Rectangle2d({
width: shape.props.w,
height: shape.props.h,
isFilled: true
})
}
component(shape: CardShape) {
return (
<HTMLContainer>
<div style={{ width: shape.props.w, height: shape.props.h }}>
{shape.props.title}
</div>
</HTMLContainer>
)
}
indicator(shape: CardShape) {
return (
<rect
width={shape.props.w}
height={shape.props.h}
/>
)
}
}
Real-world example: Arrow shape
The built-in arrow shape demonstrates advanced features:
export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
static override type = 'arrow' as const
static override props = arrowShapeProps
// Arrows can bind to other shapes
canBind({ toShape }: TLShapeUtilCanBindOpts) {
return toShape.type !== 'arrow'
}
getGeometry(shape: TLArrowShape) {
const info = getArrowInfo(shape)
return new Group2d({
children: [
new Polyline2d({ points: info.bodyPath }),
// Add arrowhead geometry
info.startArrowhead && new Polygon2d({
points: info.startArrowhead.vertices
}),
info.endArrowhead && new Polygon2d({
points: info.endArrowhead.vertices
})
].filter(Boolean)
})
}
component(shape: TLArrowShape) {
const info = getArrowInfo(shape)
return (
<SVGContainer>
<path
d={getArrowBodyPath(info)}
stroke={shape.props.color}
strokeWidth={STROKE_SIZES[shape.props.size]}
/>
{/* Render arrowheads */}
</SVGContainer>
)
}
// Custom handles for arrow endpoints
getHandles(shape: TLArrowShape) {
return [
{ id: 'start', type: 'vertex', x: shape.props.start.x, y: shape.props.start.y },
{ id: 'end', type: 'vertex', x: shape.props.end.x, y: shape.props.end.y }
]
}
onHandleDrag(shape: TLArrowShape, { handle, delta }: TLHandleDragInfo) {
const updates = { ...shape.props }
if (handle.id === 'start') {
updates.start = {
x: shape.props.start.x + delta.x,
y: shape.props.start.y + delta.y
}
}
return { ...shape, props: updates }
}
}
Best practices
Keep geometry simple: Complex geometry calculations can slow down the editor. Use simple bounding boxes for hit testing when possible.
Memoize expensive calculations: Use useMemo or computed values for calculations in the component method.
Style consistency: Use the built-in style props (color, size, etc.) for consistent appearance across shapes.
Testing: Always test your shapes with selection, resizing, rotation, and grouping to ensure they behave correctly.
- Editor - Working with the Editor API
- Bindings - Connecting shapes together
- Tools - Creating custom shape creation tools
- Shape API - Complete shape API reference