Back to Tldraw

Click detection

apps/docs/content/sdk-features/click-detection.mdx

5.2.17.8 KB
Original Source

In tldraw, the click detection system turns a pair of nearby clicks into a double_click event. The ClickManager tracks consecutive pointer down events using a state machine, dispatching a double-click event when timing and distance thresholds are met.

After a double-click, extra nearby clicks are treated as overflow. Overflow clicks do not dispatch additional click events; they suppress the sequence long enough to prevent a rapid double-double-click from becoming two double-clicks.

How it works

When a pointer down event occurs, the manager either starts a new sequence, detects a double-click, or moves the sequence into overflow. Each state has a timeout that determines how long to wait before returning to idle.

Two timeout durations control the detection speed. The first click uses doubleClickDurationMs (450ms by default), giving users time to initiate a double-click sequence. After a double-click, multiClickDurationMs (200ms by default) controls both the settle delay and the overflow suppression window. The option name is historical; it now controls only the post-double-click window.

State transitions

The click state machine progresses through these states:

StateDescription
idleNo active click sequence
pendingDoubleFirst click registered, waiting for second
pendingOverflowDouble-click registered, waiting for overflow
overflowExtra clicks detected after the double-click

The second pointer down dispatches a double_click event immediately and starts waiting for overflow. If the timeout expires before another click, the manager dispatches a double-click settle event and returns to idle. If another pointer down arrives first, the sequence moves to overflow and no further click events are dispatched until the overflow timeout expires.

Distance validation

Consecutive clicks must occur within a maximum distance of 40 pixels (screen space). If pointer down events are farther apart, the new pointer down starts a fresh click sequence instead.

Click event phases

Click events are dispatched with a phase property indicating when in the sequence the event fired:

PhaseWhen it fires
downOn the second pointer down, when the double-click is detected
upOn the matching pointer up while the double-click is still pending
settle-downWhen the timeout expires without overflow while the pointer is down
settle-upWhen the timeout expires without overflow after the pointer is up

The phase system lets tools respond at different points in the click sequence. Most default selection and cropping behavior responds on the down phase, so it starts on the second pointer down. The hand tool's double-click zoom responds on settle-up, so an overflow click can still cancel the pending zoom.

Movement cancellation

If the pointer moves too far during a pending click sequence, the system cancels the sequence and returns to idle. This prevents double-click detection during click-drag operations. The movement threshold differs for coarse pointers (touchscreens) and fine pointers (mouse, stylus).

Handling click events

Tools receive click events through handler methods defined in the TLEventHandlers interface. Here's a complete example of a custom tool that zooms in as soon as a double-click is detected:

tsx
import { StateNode, TLClickEventInfo, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

class ZoomTool extends StateNode {
	static override id = 'zoom'

	override onDoubleClick(info: TLClickEventInfo) {
		if (info.phase !== 'down') return
		this.editor.zoomIn(info.point, { animation: { duration: 200 } })
	}
}

const customTools = [ZoomTool]

export default function App() {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw
				tools={customTools}
				onMount={(editor) => {
					editor.setCurrentTool('zoom')
				}}
			/>
		</div>
	)
}

Tools can handle StateNode#onDoubleClick events. The handler receives a TLClickEventInfo object with details about the click. Use phase: 'down' for behavior that should start on the second pointer down, or a settle phase for behavior that should wait until the overflow window has passed.

The select tool routes shape double-clicks to ShapeUtil#onDoubleClick. Return a partial shape object to apply changes:

tsx
override onDoubleClick(shape: MyShape) {
	return {
		id: shape.id,
		type: shape.type,
		props: { expanded: !shape.props.expanded },
	}
}

The TLClickEventInfo type includes these properties:

PropertyTypeDescription
type'click'Event type identifier
name'double_click'Which click event this is
pointVecLikePointer position in client space
pointerIdnumberUnique identifier for the pointer
buttonnumberMouse button (0 = left, 1 = middle, 2 = right)
phase'down' | 'up' | 'settle-down' | 'settle-up'When in the click sequence this fired
target'canvas' | 'selection' | 'shape' | 'handle'What was clicked
shapeTLShape | undefinedThe shape, when target is 'shape' or 'handle'
handleTLHandle | undefinedThe handle, when target is 'handle'
shiftKeybooleanWhether Shift was held
altKeybooleanWhether Alt/Option was held
ctrlKeybooleanWhether Control was held
metaKeybooleanWhether Meta/Command was held
accelKeybooleanPlatform accelerator key (Cmd on Mac, Ctrl on Windows)

Timing configuration

Click timing is configured through the editor's options:

OptionDefaultDescription
doubleClickDurationMs450msTime window for the first click to become a double-click
multiClickDurationMs200msDouble-click settle delay and overflow suppression window
  • Canvas events — logs pointer events including click sequences to understand the event flow
  • Custom double-click behavior — overrides the default double-click handler in the SelectTool
  • Custom shape — implements onDoubleClick and other click handlers in custom shapes