--- 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.