--- title: Click detection created_at: 12/20/2025 updated_at: 12/29/2025 keywords: - click - double-click - overflow click - pointer - ClickManager status: published date: 12/20/2025 readability: 9 voice: 9 completeness: 9 accuracy: 10 notes: Could add a simpler opening hook per voice guide ("Have five minutes?"-style). --- In tldraw, the click detection system turns a pair of nearby clicks into a `double_click` event. The [ClickManager](?) tracks consecutive pointer downs with a small state machine and emits `double_click` when the 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), which is how long the user has to make the second click. 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: | State | Description | | ----------------- | --------------------------------------------- | | `idle` | No active click sequence | | `pendingDouble` | First click registered, waiting for second | | `pendingOverflow` | Double-click registered, waiting for overflow | | `overflow` | Extra clicks detected after the double-click | The second pointer down still reaches the state chart as a `pointer_down`, followed immediately by a `double_click` event, and the manager 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 Each click event carries a `phase` that says when in the sequence it fired: | Phase | When it fires | | ------------- | ------------------------------------------------------------------- | | `down` | On the second pointer down, when the double-click is detected | | `up` | On the matching pointer up while the double-click is still pending | | `settle-down` | When the timeout expires without overflow while the pointer is down | | `settle-up` | When 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 is `dragDistanceSquared` for fine pointers (mouse, stylus) and `coarseDragDistanceSquared` for coarse pointers (touchscreens), both editor [options](/sdk-features/options). The manager only sees pointer events that match the current pen mode: in pen mode, finger taps don't produce double-clicks, and outside pen mode, pen taps don't. ## 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 // info.point is in client space; zoomIn wants a point relative to the viewport const screenPoint = this.editor.inputs.getCurrentScreenPoint() this.editor.zoomIn(screenPoint, { animation: { duration: 200 } }) } } const customTools = [ZoomTool] export default function App() { return (
{ editor.setCurrentTool('zoom') }} />
) } ``` 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](?), and double-clicks on handles, edges, and corners to [ShapeUtil#onDoubleClickHandle](?), [ShapeUtil#onDoubleClickEdge](?), and [ShapeUtil#onDoubleClickCorner](?). 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: | Property | Type | Description | | ----------- | ------------------------------------------------------------- | ------------------------------------------------------ | | `type` | `'click'` | Event type identifier | | `name` | `'double_click'` | Which click event this is | | `point` | `VecLike` | Pointer position in client space | | `pointerId` | `number` | Unique identifier for the pointer | | `button` | `number` | Mouse 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' \| 'overlay'` | What was clicked | | `shape` | `TLShape \| undefined` | The shape, when target is `'shape'` or `'handle'` | | `handle` | `TLHandle \| TLSelectionHandle \| undefined` | The handle, when target is `'handle'` or `'selection'` | | `overlay` | `TLOverlay \| undefined` | The overlay, when target is `'overlay'` | | `shiftKey` | `boolean` | Whether Shift was held | | `altKey` | `boolean` | Whether Alt/Option was held | | `ctrlKey` | `boolean` | Whether Control (or Command) was held | | `metaKey` | `boolean` | Whether Meta/Command was held | | `accelKey` | `boolean` | Platform accelerator key (Cmd on Mac, Ctrl on Windows) | ## Timing configuration Click timing is configured through the editor's [options](/sdk-features/options): | Option | Default | Description | | ----------------------- | ------- | --------------------------------------------------------- | | `doubleClickDurationMs` | 450ms | Time window for the first click to become a double-click | | `multiClickDurationMs` | 200ms | Double-click settle delay and overflow suppression window | ## Related examples - [Canvas events](/examples/events/canvas-events) — logs pointer events including click sequences to understand the event flow - [Custom double-click behavior](/examples/events/custom-double-click-behavior) — overrides the default double-click handler in the SelectTool - [Shape with onDoubleClickEdge](/examples/shapes/tools/shape-with-onDoubleClickEdge) — implements `onDoubleClickEdge` in a custom shape