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
286 lines
9.5 KiB
Plaintext
286 lines
9.5 KiB
Plaintext
---
|
|
title: Events
|
|
created_at: 12/26/2025
|
|
updated_at: 12/29/2025
|
|
keywords:
|
|
- events
|
|
- EventEmitter
|
|
- TLEventMap
|
|
- change
|
|
- tick
|
|
- mount
|
|
status: published
|
|
date: 12/26/2025
|
|
readability: 9
|
|
voice: 9
|
|
completeness: 9
|
|
accuracy: 9
|
|
notes: Input events table could mention TLEventInfo link more prominently. Add API links for types like HistoryEntry.
|
|
---
|
|
|
|
The editor emits events for input, store changes, and lifecycle moments. Subscribe with `editor.on()` and unsubscribe with `editor.off()`. Use events to build analytics, sync external state, or extend editor behavior.
|
|
|
|
## Subscribing to events
|
|
|
|
The [Editor](?) extends EventEmitter, and every event name and payload is typed by [TLEventMap](?):
|
|
|
|
```tsx
|
|
import { Tldraw } from 'tldraw'
|
|
import 'tldraw/tldraw.css'
|
|
|
|
export default function App() {
|
|
return (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
onMount={(editor) => {
|
|
editor.on('event', (info) => {
|
|
console.log('Event:', info.type, info.name)
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
Unsubscribe by calling `off()` with the same handler:
|
|
|
|
```tsx
|
|
const handleEvent: TLEventMapHandler<'event'> = (info) => {
|
|
console.log('Event:', info.type)
|
|
}
|
|
|
|
editor.on('event', handleEvent)
|
|
editor.off('event', handleEvent)
|
|
```
|
|
|
|
Unsubscribe when the listener outlives the code that registered it. In React, return a cleanup function from your effect:
|
|
|
|
```tsx
|
|
useEffect(() => {
|
|
const handleChange: TLEventMapHandler<'change'> = (entry) => {
|
|
console.log('Store changed:', entry.changes)
|
|
}
|
|
|
|
editor.on('change', handleChange)
|
|
return () => editor.off('change', handleChange)
|
|
}, [editor])
|
|
```
|
|
|
|
## Event categories
|
|
|
|
### Input events
|
|
|
|
The `event` and `before-event` events fire for every event the editor dispatches: user input plus the `misc` events (`cancel`, `complete`, `interrupt`, `tick`) that tools use internally. Each receives a [TLEventInfo](?) object describing the event.
|
|
|
|
```tsx
|
|
editor.on('event', (info) => {
|
|
if (info.type === 'pointer' && info.name === 'pointer_down') {
|
|
console.log('Clicked at', info.point)
|
|
}
|
|
})
|
|
```
|
|
|
|
`before-event` fires before the event reaches the tool state machine, and `event` fires after the tools have handled it.
|
|
|
|
Events have different types:
|
|
|
|
| Type | Names | Description |
|
|
| ---------- | ----------------------------------------------------------------------------------------- | ----------------------------------- |
|
|
| `pointer` | `pointer_down`, `pointer_move`, `pointer_up`, `right_click`, `middle_click`, `long_press` | Mouse, touch, and pen interactions |
|
|
| `click` | `double_click` | Double-click sequences |
|
|
| `keyboard` | `key_down`, `key_up`, `key_repeat` | Keyboard input |
|
|
| `wheel` | `wheel` | Scroll wheel and trackpad scrolling |
|
|
| `pinch` | `pinch_start`, `pinch`, `pinch_end` | Two-finger pinch gestures |
|
|
| `misc` | `cancel`, `complete`, `interrupt`, `tick` | Internal tool lifecycle events |
|
|
|
|
Pointer events include the target—what the pointer is over: `canvas`, `shape`, `selection`, `handle`, or `overlay`.
|
|
|
|
```tsx
|
|
editor.on('event', (info) => {
|
|
if (info.type === 'pointer' && info.name === 'pointer_down') {
|
|
switch (info.target) {
|
|
case 'canvas':
|
|
console.log('Clicked empty canvas')
|
|
break
|
|
case 'shape':
|
|
console.log('Clicked shape:', info.shape.id)
|
|
break
|
|
case 'selection':
|
|
console.log('Clicked selection bounds')
|
|
break
|
|
case 'handle':
|
|
console.log('Clicked handle on:', info.shape.id)
|
|
break
|
|
case 'overlay':
|
|
console.log('Clicked overlay:', info.overlay.type)
|
|
break
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
### Shape events
|
|
|
|
Shape events fire from [Editor#createShapes](?), [Editor#updateShapes](?), and [Editor#deleteShapes](?), just before the store is written. Changes from other sources (remote sync, undo/redo, direct `store.put` calls) don't fire them—use the `change` event for those.
|
|
|
|
| Event | Payload | Description |
|
|
| ---------------- | ------------- | -------------------------------- |
|
|
| `created-shapes` | `TLRecord[]` | Shapes about to be added |
|
|
| `edited-shapes` | `TLRecord[]` | Shapes about to be updated |
|
|
| `deleted-shapes` | `TLShapeId[]` | Shapes about to be removed |
|
|
| `edit` | None | Fires alongside any of the above |
|
|
|
|
```tsx
|
|
editor.on('created-shapes', (shapes) => {
|
|
console.log('Created:', shapes.length, 'shapes')
|
|
})
|
|
|
|
editor.on('deleted-shapes', (ids) => {
|
|
console.log('Deleted:', ids.length, 'shapes')
|
|
})
|
|
```
|
|
|
|
### Store changes
|
|
|
|
The `change` event fires whenever the store updates. It receives a [HistoryEntry](?) containing the diff and source:
|
|
|
|
```tsx
|
|
editor.on('change', (entry) => {
|
|
const { added, updated, removed } = entry.changes
|
|
|
|
for (const record of Object.values(added)) {
|
|
if (record.typeName === 'shape') {
|
|
console.log('Added shape:', record.type)
|
|
}
|
|
}
|
|
|
|
for (const [from, to] of Object.values(updated)) {
|
|
if (from.typeName === 'shape') {
|
|
console.log('Updated shape:', from.id)
|
|
}
|
|
}
|
|
|
|
for (const record of Object.values(removed)) {
|
|
if (record.typeName === 'shape') {
|
|
console.log('Removed shape:', record.id)
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
The `source` property indicates where the change originated:
|
|
|
|
```tsx
|
|
editor.on('change', (entry) => {
|
|
if (entry.source === 'user') {
|
|
// Change from local user interaction
|
|
scheduleAutosave()
|
|
} else if (entry.source === 'remote') {
|
|
// Change from collaboration sync
|
|
}
|
|
})
|
|
```
|
|
|
|
### Frame events
|
|
|
|
Two events fire on every animation frame, both carrying the milliseconds elapsed since the previous frame:
|
|
|
|
| Event | Payload | Description |
|
|
| ------- | -------- | -------------------------------------------------------- |
|
|
| `frame` | `number` | Fires first; used internally (for example, for velocity) |
|
|
| `tick` | `number` | Fires immediately after `frame` |
|
|
|
|
```tsx
|
|
editor.on('tick', (elapsed) => {
|
|
// Update animations, physics, etc.
|
|
updateParticleSystem(elapsed)
|
|
})
|
|
```
|
|
|
|
These fire frequently (60+ times per second). Keep handlers fast to avoid dropping frames.
|
|
|
|
### Lifecycle events
|
|
|
|
| Event | Payload | Description |
|
|
| --------- | -------------------- | ---------------------------- |
|
|
| `mount` | None | Editor finished initializing |
|
|
| `unmount` | None | Editor component unmounted |
|
|
| `dispose` | None | Editor is being cleaned up |
|
|
| `crash` | `{ error: unknown }` | Editor encountered an error |
|
|
| `update` | None | A store operation completed |
|
|
|
|
```tsx
|
|
editor.on('mount', () => {
|
|
console.log('Editor ready')
|
|
})
|
|
|
|
editor.on('crash', ({ error }) => {
|
|
reportError(error)
|
|
})
|
|
```
|
|
|
|
### UI and camera events
|
|
|
|
| Event | Payload | Description |
|
|
| ----------------------- | ------------------------- | -------------------------------------- |
|
|
| `resize` | `BoxModel` | Viewport dimensions changed |
|
|
| `stop-camera-animation` | None | Camera animation interrupted |
|
|
| `stop-following` | None | Stopped following another user |
|
|
| `select-all-text` | `{ shapeId: TLShapeId }` | Editing started with all text selected |
|
|
| `place-caret` | `{ shapeId, point }` | Text caret positioned |
|
|
| `max-shapes` | `{ name, pageId, count }` | Page reached shape limit |
|
|
|
|
```tsx
|
|
editor.on('resize', (bounds) => {
|
|
console.log('Canvas size:', bounds.w, 'x', bounds.h)
|
|
})
|
|
|
|
editor.on('max-shapes', ({ pageId, count }) => {
|
|
showWarning(`Page has reached the ${count} shape limit`)
|
|
})
|
|
```
|
|
|
|
## UI events
|
|
|
|
The `Tldraw` component's `onUiEvent` prop captures high-level UI interactions separately from canvas events. This includes toolbar selections, menu actions, and keyboard shortcuts.
|
|
|
|
```tsx
|
|
<Tldraw
|
|
onUiEvent={(name, data) => {
|
|
console.log('UI event:', name, data)
|
|
}}
|
|
/>
|
|
```
|
|
|
|
UI events track actions like selecting tools, grouping shapes, toggling dark mode, and zooming. They fire regardless of whether the action came from a click or keyboard shortcut. For the full list of events, see [TLUiEventMap](?).
|
|
|
|
## Listening to store changes directly
|
|
|
|
The `change` event is `editor.store.listen()` with no filters. For fine-grained control, call `listen()` directly:
|
|
|
|
```tsx
|
|
const cleanup = editor.store.listen(
|
|
(entry) => {
|
|
// Handle changes
|
|
},
|
|
{ source: 'user', scope: 'all' }
|
|
)
|
|
|
|
// Later, unsubscribe
|
|
cleanup()
|
|
```
|
|
|
|
The `listen()` method accepts filter options:
|
|
|
|
- `source`: `'user'`, `'remote'`, or `'all'`—filter by change origin
|
|
- `scope`: `'all'`, `'document'`, `'session'`, or `'presence'`—filter by record scope
|
|
|
|
See [Side effects](/sdk-features/side-effects) for registering handlers that can intercept and modify changes.
|
|
|
|
## Related examples
|
|
|
|
- [Canvas events](/examples/events/canvas-events) - Log pointer, keyboard, and wheel events as you interact with the canvas.
|
|
- [Store events](/examples/events/store-events) - Track shape creation, updates, and deletion through store change events.
|
|
- [UI events](/examples/events/ui-events) - Capture high-level UI interactions like tool selection and menu actions.
|