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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
---
title: Tools
created_at: 12/20/2025
updated_at: 12/29/2025
keywords:
- tools
- StateNode
- state machine
- events
- interaction
- overrides
- custom tools
- toolbar
- tool lock
status: published
date: 12/20/2025
readability: 8
voice: 9
completeness: 9
accuracy: 10
notes: ''
---
Tools in tldraw define how the editor responds to user input. Each tool handles one interaction mode: selecting shapes, drawing, panning the canvas. The editor has a single active tool at any time and routes all input events through it. When you click the hand icon in the toolbar, the editor transitions from the select tool to the hand tool, and the canvas starts responding to drags by panning.
You implement tools as state machines using the [StateNode](?) class. Multi-step interactions map onto child states: when you resize a shape with the select tool, the tool moves through `idle`, `pointing_resize_handle`, and `resizing`. Each state handles different events and can transition to other states.
## How it works
Tools are organized in a hierarchical state machine where each node can handle events and contain child states. The editor creates a root state that contains all tools as children. When an input event occurs, it flows down from the root through the currently active tool and its active child state.
Each state node has an id, optional children, and methods for handling events. State nodes come in three types: root nodes that contain tools, branch nodes that have child states, and leaf nodes that perform actual work. Tools themselves are typically branch nodes with child states representing different phases of an interaction.
When a state becomes active, its `onEnter` method runs. When it becomes inactive, its `onExit` method runs. Between these lifecycle events, the state handles input through event methods like `onPointerDown`, `onPointerMove`, and `onKeyDown`. A state that doesn't implement a handler skips the event, and the event still continues down to the active child state, so a parent and its child can both respond.
You trigger transitions between states explicitly through [StateNode#transition](?). When the select tool's idle state detects a pointer down on a shape, it calls `this.parent.transition('pointing_shape', info)` to move to the pointing state. The transition runs the old state's `onExit` and the new state's `onEnter`.
## Key concepts
### State hierarchy
Tools exist in a tree structure starting from a root node. The root contains all available tools like select, hand, eraser, and draw. Each tool can contain child states for different phases of its interaction. For example, the select tool has children including `idle`, `pointing_shape`, `translating`, `resizing`, and `rotating`. When the select tool is active and the user starts dragging a shape, the active path becomes `select.translating`.
The hierarchy lets a tool share behavior across its child states. The select tool's `onEnter` and `onExit` set up and tear down state that every child uses, while the child states handle pointer and keyboard interactions.
### Event handling
State nodes implement event handler methods that match input event types. The handlers receive an info object containing event details like pointer position, keyboard modifiers, and the event target. The full set is `onPointerDown`, `onPointerMove`, `onPointerUp`, `onLongPress`, `onDoubleClick`, `onRightClick`, `onMiddleClick`, `onKeyDown`, `onKeyUp`, `onKeyRepeat`, `onWheel`, `onCancel`, `onComplete`, `onInterrupt`, and `onTick` for animation frame updates.
The hand tool's dragging state implements `onPointerMove` to update the camera position as the user drags.
### State transitions
You can transition to a direct child using just its id, or to deeper descendants using dot notation like `'crop.pointing_crop_handle'`.
Transitions carry information through their second parameter. When transitioning from idle to pointing, the pointer event info passes along so the pointing state knows where the interaction started. This data is available in both the exit handler of the old state and the enter handler of the new state.
### Tool registration
Tools are registered with the editor through the root state. The `@tldraw/editor` package provides only the root state with no tools. The `tldraw` package adds its full suite of tools. Custom tools are added through the `tools` prop, described below.
[Editor#setCurrentTool](?) transitions the root state to a different tool by id. [Editor#getCurrentTool](?) returns the active tool state node, and [Editor#getCurrentToolId](?) returns its id.
### Event targets
Event info objects include a `target` property indicating what the user interacted with: `canvas`, `shape`, `handle`, `selection`, or `overlay`. The canvas dispatches every pointer event with `target: 'canvas'`. The select tool's idle state hit-tests the pointer position and re-dispatches the event to itself with a more specific target, then transitions to the matching child state: `pointing_shape` for a shape, `pointing_canvas` for empty canvas.
Custom tools that need to know what's under the pointer do their own hit testing with methods like [Editor#getShapeAtPoint](?).
### Tool lock
Tool lock keeps the current tool active after completing an action. Normally, tools like geo, arrow, or note return to the select tool after creating a shape. With tool lock enabled, the tool stays active so you can create multiple shapes without reselecting the tool each time.
Tool lock is stored in instance state:
```ts
// Check if tool lock is enabled
editor.getInstanceState().isToolLocked
// Enable tool lock
editor.updateInstanceState({ isToolLocked: true })
// Toggle tool lock
const current = editor.getInstanceState().isToolLocked
editor.updateInstanceState({ isToolLocked: !current })
```
Tool lock is not enforced by the state machine. Custom tools check `isToolLocked` themselves when deciding where to go after completing their action:
```ts
if (this.editor.getInstanceState().isToolLocked) {
this.parent.transition('idle')
} else {
this.editor.setCurrentTool('select')
}
```
## Creating custom tools
To create a custom tool, extend the [StateNode](?) class and implement the static properties and event handlers you need. The simplest tool has no child states and handles events directly. Register it with the `tools` prop:
```tsx
import { StateNode, TLPointerEventInfo, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
class MeasureTool extends StateNode {
static override id = 'measure'
override onEnter() {
this.editor.setCursor({ type: 'cross', rotation: 0 })
}
override onPointerDown(info: TLPointerEventInfo) {
const start = this.editor.inputs.getCurrentPagePoint()
// Start measuring from this point
}
override onPointerUp(info: TLPointerEventInfo) {
// Finalize measurement and return to select tool
this.editor.setCurrentTool('select')
}
}
const customTools = [MeasureTool]
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw tools={customTools} />
</div>
)
}
```
Define the tools array once, outside the component or in a `useMemo`, so the editor isn't recreated on each render.
The `StateNode` class has these static properties:
| Property | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | Required. The unique identifier for this state |
| `initial` | The id of the initial child state (required if the tool has children) |
| `children()` | A function returning an array of child state constructors |
| `isLockable` | Whether the toolbar shows the tool-lock toggle while this tool is active (default: `true`). The tool itself still has to check `isToolLocked`. |
| `useCoalescedEvents` | Whether to receive the browser's coalesced pointer move events for higher-fidelity input, as the draw tool does (default: `false`; always off on iOS) |
Tools with multiple phases use child states. A tool with children sets `initial` and `children()`:
```typescript
import { StateNode } from 'tldraw'
export class StampTool extends StateNode {
static override id = 'stamp'
static override initial = 'idle'
static override children() {
return [StampIdle, StampPointing]
}
}
```
Child states follow the same pattern but focus on specific phases of the interaction. A drawing tool might have idle, pointing, and drawing states. The pointing state waits to see if the user is clicking or starting a drag, then transitions accordingly:
```typescript
export class DrawingPointing extends StateNode {
static override id = 'pointing'
override onPointerMove(info: TLPointerEventInfo) {
if (this.editor.inputs.getIsDragging()) {
this.parent.transition('drawing', info)
}
}
override onPointerUp(info: TLPointerEventInfo) {
this.parent.transition('idle', info)
}
}
```
Access the editor through `this.editor` to read input state, manipulate shapes, or transition tools. Access the parent state through `this.parent` to transition between sibling states.
## Overriding default tools
You can remove tools from the UI, add custom tools to it, or register and unregister tools at runtime.
### Removing tools from the toolbar
Use the `overrides` prop ([TLUiOverrides](?)) to modify which tools appear in the UI. The `tools` function receives the current tools object and returns a modified version:
```typescript
import { Tldraw, TLUiOverrides } from 'tldraw'
const overrides: TLUiOverrides = {
tools(editor, tools, helpers) {
// Remove the text tool from the toolbar
delete tools.text
return tools
},
}
function App() {
return <Tldraw overrides={overrides} />
}
```
This removes the tool from the toolbar, its keyboard shortcut, and the menus, since all of them read from the same tools object. It doesn't remove the tool from the editor's state machine: `editor.setCurrentTool('text')` still works.
### Adding custom tools to the toolbar
When you create a custom tool, you need to add it both to the editor's state machine and to the UI. The `tools` prop registers the tool with the state machine, while `overrides.tools` adds a [TLUiToolItem](?) to the UI context. The `kbd` you set here is what registers the keyboard shortcut:
```typescript
import { Tldraw, TLUiOverrides, StateNode } from 'tldraw'
class MyTool extends StateNode {
static override id = 'my-tool'
// ... implementation
}
const overrides: TLUiOverrides = {
tools(editor, tools, helpers) {
tools['my-tool'] = {
id: 'my-tool',
icon: 'my-icon',
label: 'My Tool',
kbd: 'm',
onSelect: () => editor.setCurrentTool('my-tool'),
}
return tools
},
}
function App() {
return <Tldraw tools={[MyTool]} overrides={overrides} />
}
```
The `icon` is a key into the UI's asset URLs; a custom icon needs an `assetUrls` override, or you can reuse a built-in icon name. To make the tool appear in the toolbar, override the `Toolbar` component and include your tool item. See the [add a tool to the toolbar example](/examples/ui/add-tool-to-toolbar) for the complete implementation.
### Dynamic tool registration
Tools can be added or removed at runtime using [Editor#setTool](?) and [Editor#removeTool](?). This is useful when tool availability depends on user permissions, feature flags, or application state.
```tsx
import { useState } from 'react'
import { Editor, StateNode, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
class HeartTool extends StateNode {
static override id = 'heart'
override onPointerDown() {
// Create a heart shape at click position
}
}
function App() {
const [editor, setEditor] = useState<Editor | null>(null)
const [isEnabled, setIsEnabled] = useState(false)
const toggleTool = () => {
if (!editor) return
if (isEnabled) {
// Switch away first if currently using the tool
if (editor.getCurrentToolId() === 'heart') {
editor.setCurrentTool('select')
}
editor.removeTool(HeartTool)
} else {
editor.setTool(HeartTool)
}
setIsEnabled(!isEnabled)
}
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw onMount={setEditor} />
<button style={{ position: 'absolute', top: 64, left: 8, zIndex: 1000 }} onClick={toggleTool}>
{isEnabled ? 'Remove heart tool' : 'Add heart tool'}
</button>
</div>
)
}
```
When removing a tool, check whether the user is currently using it. If so, transition to a different tool like select to avoid leaving the editor in an invalid state. `setTool` throws if a tool with the same id is already registered.
## Related examples
- [Custom tool (sticker)](/examples/shapes/tools/custom-tool) - A tool that adds a heart sticker to the canvas when you click.
- [Custom tool with child states](/examples/shapes/tools/tool-with-child-states) - The sticker tool rebuilt with idle, pointing, and dragging child states.
- [Screenshot tool](/examples/shapes/tools/screenshot-tool) - Drag a box on the canvas and export it as an image.
- [Lasso select tool](/examples/editor-api/lasso-select-tool) - A freehand selection tool built with reactive atoms and overlays.
- [Add a tool to the toolbar](/examples/ui/add-tool-to-toolbar) - Put a custom tool in the toolbar and keyboard shortcuts dialog with custom assets.
- [Remove a tool from the toolbar](/examples/ui/remove-tool) - Remove a default tool with UI overrides.
- [Dynamic tools with setTool and removeTool](/examples/editor-api/dynamic-tools) - Add and remove tools from the state chart after initialization.