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
149 lines
8.8 KiB
Plaintext
149 lines
8.8 KiB
Plaintext
---
|
|
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 (
|
|
<div style={{ position: 'fixed', inset: 0 }}>
|
|
<Tldraw
|
|
tools={customTools}
|
|
onMount={(editor) => {
|
|
editor.setCurrentTool('zoom')
|
|
}}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
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
|