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:
227
apps/docs/content/sdk-features/scribble.mdx
Normal file
227
apps/docs/content/sdk-features/scribble.mdx
Normal file
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: Scribble
|
||||
created_at: 12/20/2025
|
||||
updated_at: 01/31/2026
|
||||
keywords:
|
||||
- scribble
|
||||
- highlight
|
||||
- ScribbleManager
|
||||
- session
|
||||
- gesture
|
||||
- visual-feedback
|
||||
status: published
|
||||
date: 01/31/2026
|
||||
readability: 8
|
||||
voice: 8
|
||||
completeness: 9
|
||||
accuracy: 10
|
||||
notes: ''
|
||||
---
|
||||
|
||||
The scribble system draws temporary freehand paths for pointer-based interactions. Use scribbles to show visual feedback during tool operations like erasing, laser pointer drawing, or scribble-brush selection. Access the system through [Editor#scribbles](?).
|
||||
|
||||
Scribbles exist only in instance state and fade out automatically after the tool operation completes. They're never persisted to the document, though they are broadcast to collaborators through presence so peers see your eraser and laser trails.
|
||||
|
||||
## How it works
|
||||
|
||||
### Scribble lifecycle
|
||||
|
||||
A scribble's `state` is one of five values from [TLScribble](?):
|
||||
|
||||
| State | Description |
|
||||
| -------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Starting | The scribble collects points until it has more than 8. This prevents flickering for very short strokes. |
|
||||
| Active | The scribble accumulates points as the pointer moves. |
|
||||
| Complete | Drawing finished but fading hasn't started. Set with [ScribbleManager#complete](?) so the end cap tapers when the pointer lifts. |
|
||||
| Stopping | The scribble fades out by progressively removing points from its tail. The manager deletes the scribble once all points clear. |
|
||||
| Paused | Defined in the schema but not used by the manager. |
|
||||
|
||||
The [ScribbleManager#tick](?) method updates all scribbles on every animation frame, handling state transitions, point management, and fade-out timing.
|
||||
|
||||
### Fade-out behavior
|
||||
|
||||
During fade-out, the scribble shrinks from the tail by removing points at regular intervals. By default (`shrink: 0.1`) the stroke width also decreases as points are removed; set `shrink: 0` to fade at constant width.
|
||||
|
||||
The `delay` property controls how long a scribble stays at full length before shrinking. Stopping a scribble caps any remaining delay at 200ms. Self-consuming scribbles (the default) remove points from the start as you draw, maintaining a constant length.
|
||||
|
||||
## Using scribbles
|
||||
|
||||
The [ScribbleManager](?) provides two APIs: a direct API for single self-consuming strokes, and a session API for when several strokes should fade together, as the laser pointer's do.
|
||||
|
||||
### Direct API
|
||||
|
||||
The direct API works well for tools like the eraser that use self-consuming scribbles:
|
||||
|
||||
```typescript
|
||||
import { StateNode, TLPointerEventInfo } from '@tldraw/editor'
|
||||
|
||||
export class Erasing extends StateNode {
|
||||
static override id = 'erasing'
|
||||
|
||||
private scribbleId = ''
|
||||
|
||||
override onEnter(info: TLPointerEventInfo) {
|
||||
const scribble = this.editor.scribbles.addScribble({
|
||||
color: 'muted-1',
|
||||
size: 12,
|
||||
})
|
||||
this.scribbleId = scribble.id
|
||||
this.pushPointToScribble()
|
||||
}
|
||||
|
||||
override onExit() {
|
||||
this.editor.scribbles.stop(this.scribbleId)
|
||||
}
|
||||
|
||||
override onPointerMove() {
|
||||
this.pushPointToScribble()
|
||||
}
|
||||
|
||||
private pushPointToScribble() {
|
||||
const { x, y } = this.editor.inputs.getCurrentPagePoint()
|
||||
this.editor.scribbles.addPoint(this.scribbleId, x, y)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[ScribbleManager#addScribble](?) takes optional configuration and returns a [ScribbleItem](?) containing the scribble's ID. [ScribbleManager#addPoint](?) ignores points less than one page unit from the previous one; pass an optional `z` value after the coordinates (default `0.5`) for pressure-based width. [ScribbleManager#stop](?) moves the scribble to the stopping state, and the manager removes it once all points clear.
|
||||
|
||||
The scribble-brush selection in the select tool uses the same API with `color: 'selection-stroke'`, `opacity: 0.32`, and `size: 12`.
|
||||
|
||||
### Session API
|
||||
|
||||
Sessions group multiple scribbles together and control how they fade. The laser pointer uses a session so that every stroke from one drawing burst fades together. Simplified from the real [LaserTool](?):
|
||||
|
||||
```typescript
|
||||
import { StateNode } from '@tldraw/editor'
|
||||
|
||||
export class LaserTool extends StateNode {
|
||||
static override id = 'laser'
|
||||
static override initial = 'idle'
|
||||
static override children() {
|
||||
return [Idle, Lasering]
|
||||
}
|
||||
|
||||
private sessionId: string | null = null
|
||||
|
||||
getSessionId(): string {
|
||||
// Reuse existing session if it's still active
|
||||
if (this.sessionId && this.editor.scribbles.isSessionActive(this.sessionId)) {
|
||||
return this.sessionId
|
||||
}
|
||||
|
||||
// Create a new session
|
||||
this.sessionId = this.editor.scribbles.startSession({
|
||||
selfConsume: false,
|
||||
idleTimeoutMs: this.editor.options.laserDelayMs,
|
||||
fadeMode: 'grouped',
|
||||
fadeEasing: 'ease-in',
|
||||
})
|
||||
|
||||
return this.sessionId
|
||||
}
|
||||
|
||||
override onCancel() {
|
||||
if (this.sessionId && this.editor.scribbles.isSessionActive(this.sessionId)) {
|
||||
this.editor.scribbles.clearSession(this.sessionId)
|
||||
this.sessionId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The idle state adds a scribble to the session with [ScribbleManager#addScribbleToSession](?) and hands its id to the lasering state:
|
||||
|
||||
```typescript
|
||||
export class Idle extends StateNode {
|
||||
static override id = 'idle'
|
||||
|
||||
override onPointerDown() {
|
||||
const sessionId = (this.parent as LaserTool).getSessionId()
|
||||
const scribble = this.editor.scribbles.addScribbleToSession(sessionId, {
|
||||
color: 'laser',
|
||||
opacity: 0.7,
|
||||
size: 4,
|
||||
taper: false,
|
||||
})
|
||||
this.parent.transition('lasering', { sessionId, scribbleId: scribble.id })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The lasering state adds points with [ScribbleManager#addPointToSession](?) and keeps the session alive with [ScribbleManager#extendSession](?):
|
||||
|
||||
```typescript
|
||||
export class Lasering extends StateNode {
|
||||
static override id = 'lasering'
|
||||
|
||||
private scribbleId = ''
|
||||
private sessionId = ''
|
||||
|
||||
override onEnter(info: { sessionId: string; scribbleId: string }) {
|
||||
this.sessionId = info.sessionId
|
||||
this.scribbleId = info.scribbleId
|
||||
this.pushPointToScribble()
|
||||
}
|
||||
|
||||
override onPointerMove() {
|
||||
this.pushPointToScribble()
|
||||
}
|
||||
|
||||
private pushPointToScribble() {
|
||||
const { x, y } = this.editor.inputs.getCurrentPagePoint()
|
||||
this.editor.scribbles.addPointToSession(this.sessionId, this.scribbleId, x, y)
|
||||
}
|
||||
|
||||
override onTick() {
|
||||
// Reset idle timeout on activity
|
||||
this.editor.scribbles.extendSession(this.sessionId)
|
||||
}
|
||||
|
||||
override onPointerUp() {
|
||||
// Mark complete to apply taper, then let session handle fade
|
||||
this.editor.scribbles.complete(this.scribbleId)
|
||||
this.parent.transition('idle')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scribble properties
|
||||
|
||||
Scribbles support these visual properties:
|
||||
|
||||
| Property | Default | Description |
|
||||
| --------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | auto-generated | Unique identifier for the scribble |
|
||||
| `color` | `'accent'` | Canvas UI color: `'accent'`, `'white'`, `'black'`, `'selection-stroke'`, `'selection-fill'`, `'laser'`, `'muted-1'` |
|
||||
| `size` | `20` | Stroke width in screen pixels |
|
||||
| `opacity` | `0.8` | Transparency from 0 to 1 |
|
||||
| `delay` | `0` | Milliseconds before shrinking starts (for self-consuming scribbles) |
|
||||
| `shrink` | `0.1` | Rate at which stroke width decreases during fade-out (0 to 1) |
|
||||
| `taper` | `true` | Whether the stroke tapers at the ends |
|
||||
|
||||
All properties have defaults, so you only need to specify what you want to change.
|
||||
|
||||
## Session options
|
||||
|
||||
When using the session API, you can configure how scribbles behave:
|
||||
|
||||
| Property | Default | Description |
|
||||
| ---------------- | ------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `id` | auto-generated | Session identifier |
|
||||
| `selfConsume` | `true` | Whether scribbles eat their own tail as you draw |
|
||||
| `idleTimeoutMs` | `0` | Auto-stop session after this many milliseconds of inactivity (0 disables) |
|
||||
| `fadeMode` | `'individual'` | How scribbles fade: `'individual'` (each on its own) or `'grouped'` (fade together) |
|
||||
| `fadeEasing` | `'linear'` or `'ease-in'` | Easing for grouped fade. Defaults to `'ease-in'` when fadeMode is `'grouped'` |
|
||||
| `fadeDurationMs` | `laserFadeoutMs` (500ms) | Duration of the fade in milliseconds |
|
||||
|
||||
When `selfConsume` is `false`, points accumulate while the session is active and only fade after the session stops. A session stops when you call [ScribbleManager#stopSession](?), or automatically after `idleTimeoutMs` of no activity. In grouped fade mode, the manager removes points from all scribbles in the session proportionally over `fadeDurationMs`; `'ease-in'` removes them slowly at first and faster toward the end. [ScribbleManager#clearSession](?) removes everything immediately, and [ScribbleManager#isSessionActive](?) tells you whether a session is still accepting points.
|
||||
|
||||
## Customizing scribble rendering
|
||||
|
||||
Scribbles are rendered on an HTML Canvas overlay via the [ScribbleOverlayUtil](?) from the `tldraw` package. To customize rendering, extend this class (keeping its static `type`) and pass it in the `overlayUtils` prop, which replaces the default util of the same type. See [Overlay utils](/sdk-features/overlay-utils) for how overlay utils are registered and replaced.
|
||||
|
||||
## Related articles
|
||||
|
||||
- [Tools](/sdk-features/tools) - How tools use state nodes and handle pointer events
|
||||
- [Overlay utils](/sdk-features/overlay-utils) - The canvas overlay system that renders scribbles
|
||||
Reference in New Issue
Block a user