Shapes
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
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'
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'
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 = [CardShapeUtil]
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.
Migrations
You can add migrations for your shape props by adding a migrations
property to your shape's util class. See the persistence docs for more information.