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
128 lines
5.0 KiB
Plaintext
128 lines
5.0 KiB
Plaintext
---
|
|
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 (
|
|
<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.
|
|
|
|
## 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
|