Back to Tldraw

Tools

apps/docs/content/sdk-features/tools.mdx

5.4.013.4 KB
Original Source

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:

PropertyDescription
idRequired. The unique identifier for this state
initialThe id of the initial child state (required if the tool has children)
children()A function returning an array of child state constructors
isLockableWhether the toolbar shows the tool-lock toggle while this tool is active (default: true). The tool itself still has to check isToolLocked.
useCoalescedEventsWhether 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 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.