---
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 (
{
editor.on('event', (info) => {
console.log('Event:', info.type, info.name)
})
}}
/>
)
}
```
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
{
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.