Documentation Index Fetch the complete documentation index at: https://mintlify.com/tldraw/tldraw/llms.txt
Use this file to discover all available pages before exploring further.
Tools in tldraw are state machines that handle user input. Create custom tools to add new interactions and behaviors to your editor.
Overview
Tools are implemented as StateNode classes that respond to user events like pointer clicks, drags, and keyboard input. Tools can have simple single-state behavior or complex multi-state interactions.
Create a basic tool that responds to clicks:
Create the StateNode class
import { StateNode , Tldraw , toRichText } from 'tldraw'
class StickerTool extends StateNode {
static override id = 'sticker'
override onEnter () {
this . editor . setCursor ({ type: 'cross' , rotation: 0 })
}
override onPointerDown () {
const currentPagePoint = this . editor . inputs . getCurrentPagePoint ()
this . editor . createShape ({
type: 'text' ,
x: currentPagePoint . x - 12 ,
y: currentPagePoint . y - 12 ,
props: { richText: toRichText ( '❤️' ) },
})
}
}
Register the tool
Pass the tool to the Tldraw component: const customTools = [ StickerTool ]
export default function MyApp () {
return (
< Tldraw
tools = { customTools }
initialState = "sticker"
/>
)
}
Tools have several lifecycle methods:
class MyTool extends StateNode {
static override id = 'my-tool'
// Called when the tool becomes active
override onEnter () {
this . editor . setCursor ({ type: 'cross' , rotation: 0 })
}
// Called when switching to another tool
override onExit () {
this . editor . setCursor ({ type: 'default' , rotation: 0 })
}
// Called when the tool is interrupted (e.g., by pressing Escape)
override onInterrupt () {
this . cleanup ()
}
// Called when the user cancels (e.g., right-click or Escape)
override onCancel () {
this . cleanup ()
}
private cleanup () {
// Cleanup logic
this . parent . transition ( 'select' , {})
}
}
Pointer events
Handle mouse and touch input:
class DrawingTool extends StateNode {
static override id = 'drawing'
override onPointerDown () {
const { currentPagePoint } = this . editor . inputs
// Start drawing at this point
}
override onPointerMove () {
const { currentPagePoint } = this . editor . inputs
// Update drawing as pointer moves
}
override onPointerUp () {
// Finish drawing
this . parent . transition ( 'select' , {})
}
override onDoubleClick () {
// Handle double-click
}
override onTripleClick () {
// Handle triple-click
}
override onQuadrupleClick () {
// Handle quadruple-click
}
}
Keyboard events
Respond to keyboard input:
class KeyboardTool extends StateNode {
static override id = 'keyboard-tool'
override onKeyDown () {
switch ( this . editor . inputs . currentKey ) {
case 'Escape' :
this . parent . transition ( 'select' , {})
break
case 'Enter' :
this . confirmAction ()
break
}
}
override onKeyUp () {
// Handle key release
}
override onKeyRepeat () {
// Handle key held down
}
}
Complex tools use child states to manage different interaction phases:
Create the parent tool
import { StateNode } from 'tldraw'
import { ScreenshotIdle } from './ScreenshotIdle'
import { ScreenshotPointing } from './ScreenshotPointing'
import { ScreenshotDragging } from './ScreenshotDragging'
export class ScreenshotTool extends StateNode {
static override id = 'screenshot'
static override initial = 'idle'
static override children () {
return [ ScreenshotIdle , ScreenshotPointing , ScreenshotDragging ]
}
override onEnter () {
this . editor . setCursor ({ type: 'cross' , rotation: 0 })
}
override onExit () {
this . editor . setCursor ({ type: 'default' , rotation: 0 })
}
override onInterrupt () {
this . complete ()
}
override onCancel () {
this . complete ()
}
private complete () {
this . parent . transition ( 'select' , {})
}
}
Create child states
Idle state (waiting for input):import { StateNode } from 'tldraw'
export class ScreenshotIdle extends StateNode {
static override id = 'idle'
override onPointerDown () {
this . parent . transition ( 'pointing' , {})
}
}
Pointing state (mouse down, not dragging yet):export class ScreenshotPointing extends StateNode {
static override id = 'pointing'
override onPointerMove () {
if ( this . editor . inputs . isDragging ) {
this . parent . transition ( 'dragging' , {})
}
}
override onPointerUp () {
this . parent . transition ( 'idle' , {})
}
}
Dragging state (actively dragging):export class ScreenshotDragging extends StateNode {
static override id = 'dragging'
override onPointerMove () {
// Update drag selection
}
override onPointerUp () {
this . captureScreenshot ()
this . parent . transition ( 'idle' , {})
}
private captureScreenshot () {
// Take screenshot of selected area
}
}
Use this.parent.transition(stateName, info) to move between child states.
Accessing editor state
Tools have full access to the editor instance:
class MyTool extends StateNode {
override onPointerDown () {
// Get pointer position
const point = this . editor . inputs . getCurrentPagePoint ()
const screenPoint = this . editor . inputs . currentScreenPoint
// Check input state
const isDragging = this . editor . inputs . isDragging
const isShiftPressed = this . editor . inputs . shiftKey
const isAltPressed = this . editor . inputs . altKey
const isCtrlPressed = this . editor . inputs . ctrlKey
// Get selected shapes
const selectedShapes = this . editor . getSelectedShapes ()
const selectedIds = this . editor . getSelectedShapeIds ()
// Create shapes
this . editor . createShape ({ type: 'text' , x: point . x , y: point . y })
// Update shapes
this . editor . updateShape ({ id: shapeId , type: 'text' , props: { ... } })
// Delete shapes
this . editor . deleteShapes ([ shapeId ])
}
}
Make your tool accessible via the UI:
import { Tldraw , useTools , TldrawUiMenuItem } from 'tldraw'
const customTools = [ StickerTool ]
function CustomToolbar () {
const tools = useTools ()
return (
< div className = "custom-toolbar" >
< TldrawUiMenuItem
{ ... tools [ 'sticker' ] }
icon = "heart"
label = "Sticker"
/>
</ div >
)
}
export default function MyApp () {
return (
< Tldraw
tools = { customTools }
components = { { Toolbar: CustomToolbar } }
/>
)
}
Combine tools with custom shapes:
import { BaseBoxShapeTool } from 'tldraw'
// Define the shape util
class CounterShapeUtil extends BaseBoxShapeUtil < CounterShape > {
static override type = 'counter'
// ... shape implementation
}
// Create a tool for the shape
class CounterShapeTool extends BaseBoxShapeTool {
static override id = 'counter'
override shapeType = 'counter' as const
}
// Register both
const customShapeUtils = [ CounterShapeUtil ]
const customTools = [ CounterShapeTool ]
< Tldraw shapeUtils = { customShapeUtils } tools = { customTools } />
BaseBoxShapeTool automatically handles the drag-to-create interaction for box-shaped tools.
Animation and tick events
Run code on every frame:
class AnimatedTool extends StateNode {
static override id = 'animated-tool'
private frameCount = 0
override onTick () {
// Called every frame while the tool is active
this . frameCount ++
// Update something based on time
const time = Date . now ()
this . updateAnimation ( time )
}
private updateAnimation ( time : number ) {
// Animation logic
}
}
Customize tool behavior:
class ConfigurableTool extends StateNode {
static override id = 'configurable'
// Custom config
private config = {
snapToGrid: true ,
gridSize: 20 ,
color: '#ff0000' ,
}
override onPointerDown () {
const point = this . editor . inputs . getCurrentPagePoint ()
if ( this . config . snapToGrid ) {
point . x = Math . round ( point . x / this . config . gridSize ) * this . config . gridSize
point . y = Math . round ( point . y / this . config . gridSize ) * this . config . gridSize
}
this . editor . createShape ({
type: 'geo' ,
x: point . x ,
y: point . y ,
props: { color: this . config . color },
})
}
}
Common patterns
class ClickCreateTool extends StateNode {
static override id = 'click-create'
override onPointerDown () {
const point = this . editor . inputs . getCurrentPagePoint ()
this . editor . createShape ({
type: 'my-shape' ,
x: point . x ,
y: point . y ,
})
}
}
Use BaseBoxShapeTool for automatic drag-to-create: class DragCreateTool extends BaseBoxShapeTool {
static override id = 'drag-create'
override shapeType = 'my-box-shape' as const
}
class MultiClickTool extends StateNode {
static override id = 'multi-click'
private points : { x : number ; y : number }[] = []
override onPointerDown () {
const point = this . editor . inputs . getCurrentPagePoint ()
this . points . push ( point )
if ( this . points . length >= 3 ) {
this . createPolygon ()
this . points = []
}
}
override onKeyDown () {
if ( this . editor . inputs . currentKey === 'Escape' ) {
this . points = []
}
}
private createPolygon () {
this . editor . createShape ({
type: 'polygon' ,
props: { points: this . points },
})
}
}
Best practices
Clean up on exit Always reset state and clear temporary data in onExit or onCancel
Handle interruptions Implement onInterrupt to gracefully handle tool cancellation
Provide visual feedback Update the cursor and provide visual indicators of tool state
Keep state minimal Store only essential state in the tool; use the editor for shape data
Next steps
Custom shapes Create shapes to use with your tools
Custom UI Add your tools to the tldraw UI
Events and side effects React to tool interactions