snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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
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
This commit is contained in:
148
apps/docs/content/sdk-features/click-detection.mdx
Normal file
148
apps/docs/content/sdk-features/click-detection.mdx
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user