snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
234
apps/docs/content/sdk-features/bindings.mdx
Normal file
234
apps/docs/content/sdk-features/bindings.mdx
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
title: Bindings
|
||||
created_at: 12/20/2025
|
||||
updated_at: 12/29/2025
|
||||
keywords:
|
||||
- bindings
|
||||
- relationships
|
||||
- arrows
|
||||
- connections
|
||||
- BindingUtil
|
||||
status: published
|
||||
date: 12/20/2025
|
||||
readability: 9
|
||||
voice: 8
|
||||
completeness: 9
|
||||
accuracy: 8
|
||||
notes: Minor passive phrasing in places. Add API links using [BindingUtil](?) format.
|
||||
---
|
||||
|
||||
Bindings create persistent relationships between shapes. When you draw an arrow to a rectangle, a binding stores that connection so the arrow stays attached when you move the rectangle. Bindings power features like arrows that follow shapes, stickers that stick to other shapes, and layout constraints that keep shapes aligned.
|
||||
|
||||
The SDK handles bookkeeping for you: when you delete a shape, its bindings are cleaned up automatically, and lifecycle hooks on the binding's [BindingUtil](?) let you react to changes.
|
||||
|
||||
## How it works
|
||||
|
||||
When you create a binding, the editor stores it as a record with `fromId` and `toId` fields pointing to shape IDs. The editor maintains an index of all bindings touching each shape. When either shape changes position, transforms, or gets deleted, the binding's [BindingUtil](?) receives callbacks that can update the bound shapes accordingly.
|
||||
|
||||
The system handles several scenarios automatically:
|
||||
|
||||
- When a shape is deleted, all its bindings are removed and their binding utils receive isolation and deletion callbacks
|
||||
- When shapes are copied, only bindings between copied shapes are duplicated
|
||||
- When shapes are moved to different pages, cross-page bindings are automatically removed
|
||||
- When both bound shapes are copied or duplicated together, the binding is copied with them
|
||||
|
||||
The bindings index is a computed value that updates incrementally as bindings change. Lookups are fast and never scan all records.
|
||||
|
||||
## Key concepts
|
||||
|
||||
### Directional relationships
|
||||
|
||||
Every binding has direction. The `fromId` points to the source shape, and the `toId` points to the target shape. For arrows, the arrow is always the "from" shape and the shape it points to is the "to" shape. This directionality determines which lifecycle hooks fire and lets the system know which shape "owns" the relationship.
|
||||
|
||||
The distinction matters when shapes change. If you move a rectangle that an arrow points to, the arrow binding's `onAfterChangeToShape` hook fires. If you move the arrow itself, `onAfterChangeFromShape` fires instead.
|
||||
|
||||
### Binding records
|
||||
|
||||
A binding record ([TLBaseBinding](?)) contains just enough information to identify the relationship and store relationship-specific data:
|
||||
|
||||
```typescript
|
||||
interface TLBaseBinding<Type, Props> {
|
||||
id: TLBindingId
|
||||
typeName: 'binding'
|
||||
type: Type
|
||||
fromId: TLShapeId
|
||||
toId: TLShapeId
|
||||
props: Props
|
||||
meta: JsonObject
|
||||
}
|
||||
```
|
||||
|
||||
The `props` field holds binding-specific data. Arrow bindings store the normalized anchor point on the target shape and whether the attachment is "precise" or should snap to the shape's edge. Custom bindings can store any data appropriate to the relationship type.
|
||||
|
||||
### BindingUtil lifecycle
|
||||
|
||||
Each binding type implements a [BindingUtil](?) class that responds to events throughout the binding's lifetime:
|
||||
|
||||
| Hooks | When they fire |
|
||||
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `onBeforeCreate`, `onAfterCreate`, `onBeforeChange`, `onAfterChange` | The binding record itself is created or modified. The `onBefore*` hooks can return a replacement binding record. |
|
||||
| `onAfterChangeFromShape`, `onAfterChangeToShape` | A bound shape changes. These are the most common hooks for keeping shapes synchronized. Arrow bindings use them to update the arrow's position and parent when the target shape moves. |
|
||||
| `onBeforeDelete`, `onAfterDelete` | The binding record is removed. |
|
||||
| `onBeforeDeleteFromShape`, `onBeforeDeleteToShape` | A bound shape is about to be deleted. |
|
||||
| `onBeforeIsolateFromShape`, `onBeforeIsolateToShape` | The bound shapes are about to be separated (one is deleted, copied, or duplicated without the other). Use these to "bake in" the binding's current state before it disappears. |
|
||||
| `onOperationComplete` | All binding operations in a transaction have finished. Use it to compute aggregate updates across many related bindings. |
|
||||
|
||||
### Isolation vs deletion
|
||||
|
||||
Isolation callbacks handle a specific problem: when an arrow's target shape is deleted, the arrow shouldn't suddenly point to empty space. The `onBeforeIsolateFromShape` hook receives the binding and the `removedShape`, and lets the arrow update its terminal position to match the current attachment point before the binding is removed. The arrow then appears to "let go" of the shape naturally.
|
||||
|
||||
Isolation also occurs during copy and duplicate operations. If you copy an arrow but not its target, the copied arrow needs to convert its binding into a fixed position. The isolation callback handles this transformation.
|
||||
|
||||
Use isolation callbacks for consistency updates that should happen whenever shapes separate. Use `onBeforeDeleteFromShape` and `onBeforeDeleteToShape` for actions specific to deletion, like removing a sticker when its parent shape is deleted.
|
||||
|
||||
## API patterns
|
||||
|
||||
### Creating bindings
|
||||
|
||||
Create bindings using [Editor#createBinding](?) or [Editor#createBindings](?). You must provide the binding type, `fromId`, and `toId`. The BindingUtil supplies default props for anything you leave out. The editor checks both shapes' `canBind()` first and skips the binding if either refuses.
|
||||
|
||||
```typescript
|
||||
editor.createBinding({
|
||||
type: 'arrow',
|
||||
fromId: arrowShape.id,
|
||||
toId: targetShape.id,
|
||||
props: {
|
||||
terminal: 'end',
|
||||
normalizedAnchor: { x: 0.5, y: 0.5 },
|
||||
isPrecise: false,
|
||||
isExact: false,
|
||||
snap: 'none',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Querying bindings
|
||||
|
||||
The editor provides several methods for finding bindings: [Editor#getBinding](?), [Editor#getBindingsFromShape](?), [Editor#getBindingsToShape](?), and [Editor#getBindingsInvolvingShape](?).
|
||||
|
||||
```typescript
|
||||
// Get a specific binding by ID
|
||||
const binding = editor.getBinding(bindingId)
|
||||
|
||||
// Get all bindings where this shape is the source
|
||||
const outgoing = editor.getBindingsFromShape(shape.id, 'arrow')
|
||||
|
||||
// Get all bindings where this shape is the target
|
||||
const incoming = editor.getBindingsToShape(shape.id, 'arrow')
|
||||
|
||||
// Get all bindings involving this shape (either direction)
|
||||
const all = editor.getBindingsInvolvingShape(shape.id, 'arrow')
|
||||
```
|
||||
|
||||
### Updating and deleting bindings
|
||||
|
||||
Update bindings with [Editor#updateBinding](?), passing a partial with the binding's `id` and `type`:
|
||||
|
||||
```typescript
|
||||
editor.updateBinding({
|
||||
id: binding.id,
|
||||
type: 'arrow',
|
||||
props: { normalizedAnchor: { x: 0.8, y: 0.2 } },
|
||||
})
|
||||
```
|
||||
|
||||
Delete bindings with [Editor#deleteBinding](?), or let the system remove them automatically when shapes are deleted. Pass `isolateShapes: true` to trigger isolation callbacks:
|
||||
|
||||
```typescript
|
||||
editor.deleteBinding(binding.id, { isolateShapes: true })
|
||||
```
|
||||
|
||||
### Controlling which shapes can bind
|
||||
|
||||
Shapes control whether they accept bindings by implementing [ShapeUtil#canBind](?):
|
||||
|
||||
```typescript
|
||||
class MyShapeUtil extends ShapeUtil<MyShape> {
|
||||
static override type = 'my-shape' as const
|
||||
|
||||
override canBind({ toShape, bindingType }: TLShapeUtilCanBindOpts) {
|
||||
// Only allow arrow bindings where this shape is the target
|
||||
return bindingType === 'arrow' && toShape.type === 'my-shape'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The editor calls both shapes' `canBind()` methods before creating or updating a binding. If either returns false, the binding is skipped. Use [Editor#canBindShapes](?) to run the same check yourself.
|
||||
|
||||
## Extension points
|
||||
|
||||
Custom binding types let you create new kinds of relationships between shapes.
|
||||
|
||||
### Defining the binding type
|
||||
|
||||
First, extend the type system to include your binding's props. Use TypeScript's module augmentation to add your binding type to [TLGlobalBindingPropsMap](?), then derive the binding type from [TLBinding](?):
|
||||
|
||||
```typescript
|
||||
import { TLBinding, VecModel } from 'tldraw'
|
||||
|
||||
declare module 'tldraw' {
|
||||
export interface TLGlobalBindingPropsMap {
|
||||
myBinding: {
|
||||
anchor: VecModel
|
||||
strength: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MyBinding = TLBinding<'myBinding'>
|
||||
```
|
||||
|
||||
### Implementing BindingUtil
|
||||
|
||||
Create a class extending [BindingUtil](?) with your binding type. At minimum, implement `getDefaultProps()`. Add lifecycle hooks based on what behavior you need:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
BindingOnShapeChangeOptions,
|
||||
BindingOnShapeIsolateOptions,
|
||||
BindingUtil,
|
||||
RecordProps,
|
||||
T,
|
||||
vecModelValidator,
|
||||
} from 'tldraw'
|
||||
|
||||
class MyBindingUtil extends BindingUtil<MyBinding> {
|
||||
static override type = 'myBinding' as const
|
||||
static override props: RecordProps<MyBinding> = {
|
||||
anchor: vecModelValidator,
|
||||
strength: T.number,
|
||||
}
|
||||
|
||||
override getDefaultProps() {
|
||||
return { anchor: { x: 0.5, y: 0.5 }, strength: 1 }
|
||||
}
|
||||
|
||||
override onAfterChangeToShape({ binding, shapeAfter }: BindingOnShapeChangeOptions<MyBinding>) {
|
||||
// Update the "from" shape when the "to" shape moves
|
||||
}
|
||||
|
||||
override onBeforeIsolateFromShape({
|
||||
binding,
|
||||
removedShape,
|
||||
}: BindingOnShapeIsolateOptions<MyBinding>) {
|
||||
// Bake in current state before the binding is removed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registering the binding
|
||||
|
||||
Pass your BindingUtil to the editor via the `bindingUtils` prop:
|
||||
|
||||
```tsx
|
||||
<Tldraw bindingUtils={[MyBindingUtil]} />
|
||||
```
|
||||
|
||||
## Related examples
|
||||
|
||||
The examples app includes several binding implementations that demonstrate different use cases:
|
||||
|
||||
- [Sticker bindings](/examples/shapes/tools/sticker-bindings) - Shapes that stick to other shapes and follow them when moved. Demonstrates `onAfterChangeToShape` for position updates and `onBeforeDeleteToShape` for cascading deletion.
|
||||
- [Pin bindings](/examples/shapes/tools/pin-bindings) - Pins that connect networks of shapes together, moving them as a group. Demonstrates `onOperationComplete` for computing aggregate updates across multiple related bindings.
|
||||
- [Layout bindings](/examples/shapes/tools/layout-bindings) - Constraining shapes to layout positions. Demonstrates using bindings to enforce spatial relationships between shapes.
|
||||
- [Arrow binding options](/examples/shapes/tools/arrow-binding-options) - Configuring how the built-in arrow binding attaches to shapes.
|
||||
Reference in New Issue
Block a user