apps/docs/content/sdk-features/edge-scrolling.mdx
Edge scrolling automatically pans the camera when you drag shapes toward the viewport edges. This lets you move shapes across the canvas without releasing the drag to scroll manually.
The system activates only during drag operations. It requires three conditions: you must be dragging (not panning), the camera must be unlocked, and the pointer must be within a proximity zone at the viewport edge.
Tools that support edge scrolling call editor.edgeScrollManager.updateEdgeScrolling(elapsed) on every tick during drag operations. The manager checks the pointer position against the viewport bounds and calculates a proximity factor for each axis.
When the pointer enters the edge scroll zone, the manager tracks elapsed time. After a configurable delay, scrolling starts and gradually accelerates using an easing function. The camera moves on each tick until the pointer leaves the edge zone or the drag ends.
override onTick({ elapsed }: TLTickEventInfo) {
this.editor.edgeScrollManager.updateEdgeScrolling(elapsed)
}
The built-in select tool uses edge scrolling in three states: Translating (moving shapes), Brushing (selection box), and Resizing (dragging handles).
The manager determines edge proximity by comparing the pointer position to the viewport bounds. An edge scroll zone extends inward from each screen edge by a distance defined in editor.options.edgeScrollDistance (default: 8 pixels).
When the pointer enters this zone, the manager calculates a proximity factor from 0 to 1 based on how deeply the pointer penetrates the zone. At the zone boundary, the factor is 0. At the screen edge (or beyond), it reaches 1. Each axis is calculated independently.
For touch input, the system expands the effective pointer size using editor.options.coarsePointerWidth (default: 12 pixels). The expanded pointer area is centered on the touch point, making edge scrolling easier to trigger on mobile devices.
Edge detection respects screen insets from the editor's instance state. The insets array records whether each edge of the editor is flush with the browser window, in CSS order: [top, right, bottom, left].
When an edge is flush with the window, the pointer can't move past it, so the scroll zone extends inward from that edge. When an edge is inset—the editor is embedded with other page content beside it—the zone starts at the editor's boundary instead, and scrolling begins once the pointer moves outside the editor.
Once the pointer enters the edge zone, the manager waits for editor.options.edgeScrollDelay milliseconds (default: 200ms) before starting to scroll. This delay prevents accidental scrolling when the pointer briefly crosses the edge.
After the delay, scrolling begins with gradual acceleration controlled by editor.options.edgeScrollEaseDuration (default: 200ms). The manager applies EASINGS.easeInCubic to create smooth acceleration from zero to full speed.
The scroll speed combines several factors. The base speed comes from editor.options.edgeScrollSpeed (default: 25 pixels per tick) multiplied by the user preference from editor.user.getEdgeScrollSpeed() (default: 1).
The proximity factor (0 to 1) scales speed based on how close the pointer is to the screen edge. Scrolling is slower near the zone boundary and faster at the edge itself.
On smaller displays, a screen size factor of 0.612 applies when that viewport dimension is below 1000 pixels. This reduces speed independently for each axis. The final scroll delta divides by the current zoom level to maintain consistent canvas-space velocity.
const pxSpeed = editor.user.getEdgeScrollSpeed() * editor.options.edgeScrollSpeed
const screenSizeFactorX = screenBounds.w < 1000 ? 0.612 : 1
const screenSizeFactorY = screenBounds.h < 1000 ? 0.612 : 1
const scrollDeltaX = (pxSpeed * proximityFactor.x * screenSizeFactorX) / zoomLevel
const scrollDeltaY = (pxSpeed * proximityFactor.y * screenSizeFactorY) / zoomLevel
The camera only moves when all these conditions are met:
editor.inputs.getIsDragging() and editor.inputs.getIsPanning() before calling updateEdgeScrolling().editor.getCameraOptions().isLocked is false)When the pointer leaves the edge zone, scrolling stops and the internal duration timer resets.
You can customize edge scrolling through the editor's options:
| Option | Default | Description |
|---|---|---|
edgeScrollDelay | 200 | Milliseconds to wait before starting scroll |
edgeScrollEaseDuration | 200 | Milliseconds to accelerate from zero to full speed |
edgeScrollSpeed | 25 | Base scroll speed in pixels per tick |
edgeScrollDistance | 8 | Width of the edge scroll zone in pixels |
coarsePointerWidth | 12 | Expanded pointer size for touch input (pixels) |
Set these options when creating the editor:
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
const options = {
edgeScrollSpeed: 50, // Double the default speed
edgeScrollDelay: 100, // Start scrolling sooner
}
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw options={options} />
</div>
)
}
Edge scroll speed is also a user preference. editor.user.getEdgeScrollSpeed() returns a multiplier that defaults to 1, and you can change it with editor.user.updateUserPreferences({ edgeScrollSpeed: 2 }). The preference persists across sessions and scales all edge scrolling without changing the base configuration.
To add edge scrolling to your custom tool, call updateEdgeScrolling() in the tick handler. The manager reads the pointer position from the editor's input system, so you only need to pass the elapsed time. Like the built-in tool states, skip the call unless the user is dragging:
import { StateNode, TLTickEventInfo } from '@tldraw/editor'
export class CustomDragState extends StateNode {
static override id = 'dragging'
override onTick({ elapsed }: TLTickEventInfo) {
const { editor } = this
if (!editor.inputs.getIsDragging() || editor.inputs.getIsPanning()) return
editor.edgeScrollManager.updateEdgeScrolling(elapsed)
}
}
The manager tracks whether edge scrolling is active using getIsEdgeScrolling(). This returns true when the pointer is in the edge zone and scrolling has started (after the delay).
Only call updateEdgeScrolling() during states where edge scrolling makes sense. The built-in select tool calls it during translating, brushing, and resizing, but not during idle or pointing states.
See the custom tool example for building tools that can implement edge scrolling. For complex tools with multiple states, see the tool with child states example.