Back to Tldraw

Cursors

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

5.2.17.7 KB
Original Source

The cursor system controls what cursor users see when interacting with the canvas. The current cursor is stored in instance state and changes automatically as users hover over different elements or use different tools. You can also set the cursor manually for custom tools.

Cursor state

The cursor state consists of a type and a rotation angle. Access it through Editor#getInstanceState:

typescript
const { type, rotation } = editor.getInstanceState().cursor

The type determines the visual appearance—like 'default', 'grab', or 'nwse-resize'. The rotation is an angle in radians that rotates the cursor icon. Rotation is mainly used for resize and rotate cursors so they align with the shape being manipulated.

Cursor types

tldraw supports these cursor types:

TypeDescription
defaultStandard pointer arrow
pointerHand indicating clickable element
crossCrosshair for precise positioning
grabOpen hand for draggable content
grabbingClosed hand while dragging
textI-beam for text editing
moveFour-way arrow for moving elements
zoom-inMagnifying glass with plus
zoom-outMagnifying glass with minus
ew-resizeHorizontal resize (east-west)
ns-resizeVertical resize (north-south)
nesw-resizeDiagonal resize (northeast-southwest)
nwse-resizeDiagonal resize (northwest-southeast)
resize-edgeEdge resize (used for edge handles)
resize-cornerCorner resize (used for corner handles)
nesw-rotateRotation handle (northeast-southwest position)
nwse-rotateRotation handle (northwest-southeast position)
senw-rotateRotation handle (southeast-northwest position)
swne-rotateRotation handle (southwest-northeast position)
rotateGeneral rotation cursor
noneHidden cursor

Static cursors like default, pointer, and grab use CSS cursor values directly. Dynamic cursors like the resize and rotate types render as custom SVGs with rotation applied.

Setting the cursor

Use Editor#setCursor to change the cursor:

typescript
editor.setCursor({ type: 'cross', rotation: 0 })

You can update just the type or just the rotation—the other property keeps its current value:

typescript
// Change only the type
editor.setCursor({ type: 'grab' })

// Change only the rotation
editor.setCursor({ type: 'nwse-resize', rotation: Math.PI / 4 })

Cursor rotation

Rotation is specified in radians. When users resize or rotate shapes that are themselves rotated, the cursor rotates to match:

typescript
// Get the selection's rotation and apply it to a resize cursor
const selectionRotation = editor.getSelectionRotation()
editor.setCursor({
	type: 'nwse-resize',
	rotation: selectionRotation,
})

This keeps the cursor aligned with the shape's edges rather than the screen axes. The default tools handle cursor rotation automatically. You only need to set it manually for custom tools.

Cursors in custom tools

Custom tools typically set the cursor when entering a state and reset it when exiting:

typescript
import { StateNode } from 'tldraw'

export class MyCustomTool extends StateNode {
	static override id = 'my-tool'

	override onEnter() {
		this.editor.setCursor({ type: 'cross', rotation: 0 })
	}

	override onExit() {
		this.editor.setCursor({ type: 'default', rotation: 0 })
	}
}

For tools with child states, each state can set its own cursor. A drawing state might use 'cross', while a dragging state uses 'grabbing'.

Cursor colors

Dynamic cursors (resize and rotate types) adapt to the current color scheme. In light mode, the cursor style color is set to black. In dark mode, it's set to white. The SVG patterns themselves have fixed black and white fills for contrast. This happens automatically through the internal useCursor hook.

Collaborator cursors

In multiplayer sessions, each user's cursor appears on other users' canvases. These remote cursors use the user's presence color—a randomly assigned color from the user color palette.

User color palette

When a user first loads tldraw, they're assigned a random color from this palette:

typescript
const USER_COLORS = [
	'#FF802B',
	'#EC5E41',
	'#F2555A',
	'#F04F88',
	'#E34BA9',
	'#BD54C6',
	'#9D5BD2',
	'#7B66DC',
	'#02B1CC',
	'#11B3A3',
	'#39B178',
	'#55B467',
]

You can read or change a user's color through user preferences:

typescript
// Get the user's color
const color = editor.user.getColor()

// Set a specific color
editor.user.updateUserPreferences({ color: '#FF802B' })

Rendering collaborator cursors

Remote cursors render through CollaboratorCursorOverlayUtil, one of the default overlay utils. It draws each collaborator's cursor directly to the canvas:

  • The cursor arrow in the user's color
  • The user's name as a label next to the cursor
  • Any active chat message in a bubble

To customize the rendering, pass your own overlay util to the overlayUtils prop. An overlay util with the same static type replaces the default one:

tsx
import { CollaboratorCursorOverlayUtil, TLCollaboratorCursorOverlay, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'

class CustomCollaboratorCursorOverlayUtil extends CollaboratorCursorOverlayUtil {
	override render(ctx: CanvasRenderingContext2D, overlays: TLCollaboratorCursorOverlay[]) {
		const zoom = this.editor.getZoomLevel()
		for (const overlay of overlays) {
			const { x, y, color, name } = overlay.props

			// Draw a dot instead of the default arrow
			ctx.beginPath()
			ctx.arc(x, y, 8 / zoom, 0, Math.PI * 2)
			ctx.fillStyle = color
			ctx.fill()

			if (name) {
				ctx.font = `${12 / zoom}px sans-serif`
				ctx.fillText(name, x + 12 / zoom, y + 4 / zoom)
			}
		}
	}
}

export default function App() {
	return (
		<div style={{ position: 'fixed', inset: 0 }}>
			<Tldraw overlayUtils={[CustomCollaboratorCursorOverlayUtil]} />
		</div>
	)
}

The render context is transformed to page space, so divide screen-pixel sizes by the zoom level to keep them constant on screen. See Overlay utils for the full overlay API.

Cursor position in presence

Collaborator cursor positions are stored in presence records (TLInstancePresence). The cursor field includes position, type, and rotation:

typescript
{
  cursor: {
    x: number
    y: number
    type: TLCursorType
    rotation: number
  } | null
}

The editor automatically broadcasts cursor position updates to other users in the same room. Cursor position is null when the user's pointer is outside the canvas.

  • Cursor chat - Send ephemeral chat messages at the cursor position
  • Tools - Learn how tools handle input and set cursors
  • Collaboration - User presence and multiplayer features
  • User preferences - Manage user colors and other preferences
  • Overlay utils - Canvas overlays, including the collaborator cursor overlay