In tldraw, a shape is something that can exist on the page, like an arrow, an image, or some text.

This article is about shapes: what they are, how they work, and how to create your own shapes. If you'd prefer to see an example, see the tldraw repository's examples app for examples of how to create custom shapes in tldraw.

Types of shape

We make a distinction between three types of shapes: "core", "default", and "custom".

Core shapes

The editor's core shapes are shapes that are built in and always present. At the moment the only core shape is the group shape.

Default shapes

The default shapes are all of the shapes that are included by default in the Tldraw component, such as the TLArrowShape or TLDrawShape. They are exported from the @tldraw/tldraw library as defaultShapeUtils.

Custom shapes

Custom shapes are shapes that were created by you or someone you love. Find more information about custom shapes below.

The shape object

Shapes are just records (JSON objects) that sit in the store. For example, here's a shape record for a rectangle geo shape:

{
    "parentId": "page:somePage",
    "id": "shape:someId",
    "typeName": "shape"
    "type": "geo",
    "x": 106,
    "y": 294,
    "rotation": 0,
    "index": "a28",
    "opacity": 1,
    "isLocked": false,
    "props": {
        "w": 200,
        "h": 200,
        "geo": "rectangle",
        "color": "black",
        "labelColor": "black",
        "fill": "none",
        "dash": "draw",
        "size": "m",
        "font": "draw",
        "text": "diagram",
        "align": "middle",
        "verticalAlign": "middle",
        "growY": 0,
        "url": ""
    },
    "meta": {},
}

Base properties

Every shape contains some base information. These include the shape's type, position, rotation, opacity, and more. You can find the full list of base properties here.

Props

Every shape also contains some shape-specific information, called props. Each type of shape can have different props. For example, the props of a text shape are much different than the props of an arrow shape.

Meta

Meta information is information that is not used by tldraw but is instead used by your application. For example, you might want to store the name of the user who created a shape, or the date that the shape was created. You can find more information about meta information below.

The ShapeUtil class

While tldraw's shapes themselves are simple JSON objects, we use ShapeUtil classes to answer questions about shapes. For example, when the editor needs to render a text shape, it will find the TextShapeUtil and call its ShapeUtil.component method, passing in the text shape object as an argument.


Custom shapes

You can create your own custom shapes. In the examples below, we will create a custom "card" shape. It'll be a simple rectangle with some text inside.

For an example of how to create custom shapes, see our custom shapes example.

Shape type

In tldraw's data model, each shape is represented by a JSON object. Let's first create a type that describes what this object will look like.

import { TLBaseShape } from '@tldraw/tldraw'

type CardShape = TLBaseShape<'card', { w: number; h: number }>

With the TLBaseShape helper, we define the shape's type property (card) and the shape's props property ({ w: number, h: number }). The type can be any string but the props must be a regular JSON-serializable JavaScript object.

The TLBaseShape helper adds the other base properties of a shape, such as x, y, rotation, and opacity.

Shape Util

While tldraw's shapes themselves are simple JSON objects, we use ShapeUtil classes to answer questions about shapes.

Let's create a ShapeUtil class for the shape.

import { HTMLContainer, ShapeUtil } from '@tldraw/tldraw'

class CardShapeUtil extends ShapeUtil<CardShape> {
	static override type = 'card' as const

	getDefaultProps(): CardShape['props'] {
		return {
			w: 100,
			h: 100,
		}
	}

	getGeometry(shape: ICardShape) {
		return new Rectangle2d({
			width: shape.props.w,
			height: shape.props.h,
			isFilled: true,
		})
	}

	component(shape: CardShape) {
		return <HTMLContainer>Hello</HTMLContainer>
	}

	indicator(shape: CardShape) {
		return <rect width={shape.props.w} height={shape.props.h} />
	}
}

This is a minimal ShapeUtil. We've given it a static property type that matches the type of our shape, we've provided implementations for the abstract methods ShapeUtil.getDefaultProps, ShapeUtil.getBounds, ShapeUtil.component, and ShapeUtil.indicator.

We still have work to do on the CardShapeUtil class, but we'll come back to it later. For now, let's put the shape onto the canvas by passing it to the Tldraw component.

The shapeUtils prop

We pass an array of our shape utils into the Tldraw component's shapeUtils prop.

const MyCustomShapes = [MyCardShape]

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

We can create one of our custom card shapes using the Editor API. We'll do this by setting the onMount prop of the Tldraw component.

export default function () {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw
				shapeUtils={MyCustomShapes}
				onMount={(editor) => {
					editor.createShapes([{ type: 'card' }])
				}}
			/>
		</div>
	)
}

Once the page refreshes, we should now have our custom shape on the canvas.

Meta information

Shapes also have a meta property (see TLBaseShape.meta) that you can fill with your own data. This should feel like a bit of a hack, however it's intended to be an escape hatch for applications where you want to use tldraw's existing shapes but also want to attach a bit of extra data to the shape.

Note that tldraw's regular shape definitions have an unknown object for the shape's meta property. To type your shape's meta, use a union like this:

type MyShapeWithMeta = TLGeoShape & { meta: { createdBy: string } }

const shape = editor.getShape<MyShapeWithMeta>(myGeoShape.id)

You can update a shape's meta property in the same way you would update its props, using Editor.updateShapes.

editor.updateShapes<MyShapeWithMeta>([
	{
		id: myGeoShape.id,
		type: 'geo',
		meta: {
			createdBy: 'Steve',
		},
	},
])

Like TLBaseShape.props, the data in a TLBaseShape.meta object must be JSON serializable.

In addition to setting meta properties this way, you can also set the default meta data for shapes using the Editor's Editor.getInitialMetaForShape method.

editor.getInitialMetaForShape = (shape: TLShape) => {
	if (shape.type === 'text') {
		return { createdBy: currentUser.id, lastModified: Date.now() }
	} else {
		return { createdBy: currentUser.id }
	}
}

Whenever new shapes are created using the Editor.createShapes method, the shape's meta property will be set using the Editor.getInitialMetaForShape method. By default this method returns an empty object.

Using starter shapes

You can use "starter" shape utils like BaseBoxShapeUtil to get regular rectangular shape behavior.

Flags

You can use flags like ShapeUtil.hideRotateHandle to hide different parts of the UI when the shape is selected, or else to control different behaviors of the shape.

Interaction

You can turn on pointer-events to allow users to interact inside of the shape.

Editing

You can make shapes "editable" to help decide when they're interactive or not.

...and more!

EditorAssets