--- title: Ticks created_at: 12/20/2025 updated_at: 12/20/2025 keywords: - tick - frame - animation - TickManager - requestAnimationFrame status: published date: 12/20/2025 readability: 9 voice: 8 completeness: 9 accuracy: 10 notes: '' --- The tick system provides a frame-synchronized update loop for the editor. On every animation frame the editor emits a `frame` event and then a `tick` event, each with the elapsed time in milliseconds since the last frame. While pointer and keyboard events fire in response to user input, tick events fire continuously. Use them when you need updates that run every frame regardless of user interaction. > The editor emits `frame` first so its own bookkeeping (pointer velocity, following a collaborator) runs before `tick` handlers. Use `tick` in application code. ## Subscribing to tick events The most common way to use ticks is by subscribing to the `tick` event on the editor. The callback receives the elapsed time in milliseconds since the last frame: ```tsx import { Tldraw } from 'tldraw' import 'tldraw/tldraw.css' export default function TickExample() { return (
{ const handleTick = (elapsed: number) => { // elapsed is typically ~16ms at 60fps updateAnimation(elapsed) } editor.on('tick', handleTick) // Clean up when done return () => editor.off('tick', handleTick) }} />
) } ``` Remember to unsubscribe when your component unmounts or when you no longer need tick updates. Scale movement by `elapsed` rather than assuming a fixed framerate. For one-off work on the next frame, use `editor.timers.requestAnimationFrame`, which is cleaned up when the editor is disposed. ## Tick events in tools When building custom tools using the state machine pattern, you can handle tick events by implementing the `onTick` method on your [StateNode](?). The editor dispatches tick events through the state tree after flushing any pending pointer events for that frame, so your active tool states receive them automatically: ```typescript import { StateNode, TLTickEventInfo } from 'tldraw' export class MyDraggingState extends StateNode { static override id = 'dragging' override onTick({ elapsed }: TLTickEventInfo) { // Update something every frame while this state is active this.updateDragPosition(elapsed) } private updateDragPosition(elapsed: number) { // Your frame-based logic here } } ``` [TLTickEventInfo](?) contains the elapsed time in milliseconds. ## Edge scrolling example The most common use of `onTick` in tools is edge scrolling during drag operations. When you drag near the edge of the viewport, the canvas scrolls automatically. Here's how tldraw's built-in `Translating` state handles it: ```typescript import { StateNode, TLTickEventInfo } from 'tldraw' export class Translating extends StateNode { static override id = 'translating' override onTick({ elapsed }: TLTickEventInfo) { const { editor } = this if (!editor.inputs.getIsDragging() || editor.inputs.getIsPanning()) return editor.edgeScrollManager.updateEdgeScrolling(elapsed) } } ``` The [EdgeScrollManager](?) accumulates elapsed time to create a smooth acceleration effect. After a short delay, scrolling begins slowly and speeds up the longer you hold near the edge. For more details on edge scrolling, see the [edge scrolling](/sdk-features/edge-scrolling) documentation. ## How the editor uses ticks internally The editor uses tick and frame events for several internal features. The [ScribbleManager](?) animates the trails you see while erasing, using the laser pointer, or scribble-selecting. On each tick it adds new points to active scribbles and shrinks them from the tail, so the trail fades. The [InputsManager](?) computes pointer velocity on each `frame` event. Read it with `editor.inputs.getPointerVelocity()`; the select tool uses it to decide when to snap or drop into a container. Camera methods like `editor.zoomIn()` and `editor.zoomToFit()` animate when you pass an `animation` option. The animation subscribes to `tick` for its duration and unsubscribes when complete. ## When to use tick events Tick events are appropriate when you need continuous updates that run every frame: - Animations and interpolation that should run regardless of user input - Edge scrolling during drag operations - Physics simulations or particle systems - Debouncing based on frame counts rather than timeouts Don't use tick events for responding to user input. Pointer, keyboard, and wheel events are better for that since they fire immediately when the user acts. Tick events add a frame of latency. ## Related - [Events](/sdk-features/events) - Overview of all editor events including tick - [Edge scrolling](/sdk-features/edge-scrolling) - Detailed documentation on edge scrolling behavior - [Snowstorm example](https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples/use-cases/snowstorm) - Uses tick events to animate falling snowflakes