apps/docs/content/sdk-features/ticks.mdx
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
framefirst so its own bookkeeping (pointer velocity, following a collaborator) runs beforetickhandlers. Usetickin application code.
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:
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function TickExample() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
onMount={(editor) => {
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)
}}
/>
</div>
)
}
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.
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:
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.
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:
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 documentation.
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.
Tick events are appropriate when you need continuous updates that run every frame:
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.