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:
266
apps/docs/content/sdk-features/overlay-utils.mdx
Normal file
266
apps/docs/content/sdk-features/overlay-utils.mdx
Normal file
@@ -0,0 +1,266 @@
|
||||
---
|
||||
title: Overlay utils
|
||||
status: published
|
||||
author: steveruizok
|
||||
date: 4/6/2026
|
||||
keywords:
|
||||
- overlay
|
||||
- overlays
|
||||
- OverlayUtil
|
||||
- canvas
|
||||
- selection
|
||||
- handles
|
||||
- brush
|
||||
- scribble
|
||||
- snap
|
||||
- hit testing
|
||||
---
|
||||
|
||||
Overlay utils render ephemeral UI on the canvas: selection handles, brush rectangles, snap lines, scribbles, and shape handles. They draw directly to a `<canvas>` element using the Canvas 2D API and can optionally provide hit-test geometry for pointer interactions.
|
||||
|
||||
Each overlay util defines a specific type of canvas UI. The editor queries all registered overlay utils reactively: when the editor state changes, overlay utils determine whether they are active, produce overlay instances, and render them.
|
||||
|
||||
## How it works
|
||||
|
||||
An [OverlayUtil](?) is an abstract class with four responsibilities:
|
||||
|
||||
1. **Activation** — `isActive()` returns whether the overlay should render right now
|
||||
2. **Overlay instances** — `getOverlays()` returns the current set of overlay objects
|
||||
3. **Rendering** — `render()` draws overlays into a canvas 2D context
|
||||
4. **Hit testing** (optional) — `getGeometry()` returns geometry for interactive overlays
|
||||
|
||||
The editor calls these methods reactively. When `isActive()` returns `false`, the overlay is skipped entirely. When it returns `true`, `getOverlays()` re-runs whenever the editor state it reads changes, and `render()` re-runs on that plus camera movement and viewport resizes.
|
||||
|
||||
Paint order across utils comes from `options.zIndex`: higher numbers paint on top and are hit-tested first, ties keep registration order, and the default is `0`. Built-in utils use values like 50, 100, 200, and 300 so custom utils can slot between them.
|
||||
|
||||
## Default overlay utils
|
||||
|
||||
The `tldraw` package includes these overlay utils by default:
|
||||
|
||||
| Overlay util | Purpose |
|
||||
| ------------------------------------------ | --------------------------------------------- |
|
||||
| [ShapeIndicatorOverlayUtil](?) | Selection, hover, and hint outlines on shapes |
|
||||
| [SelectionForegroundOverlayUtil](?) | Selection box, resize handles, corners |
|
||||
| [ShapeHandleOverlayUtil](?) | Shape handles (arrows, lines, etc.) |
|
||||
| [BrushOverlayUtil](?) | Selection brush rectangle |
|
||||
| [ZoomBrushOverlayUtil](?) | Zoom brush rectangle |
|
||||
| [SnapIndicatorOverlayUtil](?) | Snap alignment guides |
|
||||
| [ScribbleOverlayUtil](?) | Eraser and lasso scribbles |
|
||||
| [ArrowHintOverlayUtil](?) | Target hints while drawing arrows |
|
||||
| [ArrowBindingHintOverlayUtil](?) | Binding hints when dragging arrow handles |
|
||||
| [CollaboratorBrushOverlayUtil](?) | Remote users' brush rectangles |
|
||||
| [CollaboratorScribbleOverlayUtil](?) | Remote users' scribbles |
|
||||
| [CollaboratorHintOverlayUtil](?) | Remote users' viewport-edge hint arrows |
|
||||
| [CollaboratorShapeIndicatorOverlayUtil](?) | Remote users' selection indicators |
|
||||
|
||||
These are exported as [defaultOverlayUtils](?) from `tldraw`.
|
||||
|
||||
## Creating an overlay util
|
||||
|
||||
Extend the [OverlayUtil](?) class and implement at least `isActive()`, `getOverlays()`, and `render()`:
|
||||
|
||||
```tsx
|
||||
import { OverlayUtil, TLOverlay } from 'tldraw'
|
||||
|
||||
interface MyHighlightOverlay extends TLOverlay {
|
||||
props: {
|
||||
x: number
|
||||
y: number
|
||||
radius: number
|
||||
}
|
||||
}
|
||||
|
||||
class HighlightOverlayUtil extends OverlayUtil<MyHighlightOverlay> {
|
||||
static override type = 'highlight'
|
||||
|
||||
override isActive(): boolean {
|
||||
// Active when there's exactly one selected shape
|
||||
return this.editor.getSelectedShapeIds().length === 1
|
||||
}
|
||||
|
||||
override getOverlays(): MyHighlightOverlay[] {
|
||||
const shape = this.editor.getOnlySelectedShape()
|
||||
if (!shape) return []
|
||||
|
||||
const bounds = this.editor.getShapePageBounds(shape)
|
||||
if (!bounds) return []
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'highlight',
|
||||
type: 'highlight',
|
||||
props: {
|
||||
x: bounds.midX,
|
||||
y: bounds.midY,
|
||||
radius: Math.max(bounds.width, bounds.height) / 2 + 20,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
override render(ctx: CanvasRenderingContext2D, overlays: MyHighlightOverlay[]): void {
|
||||
const zoom = this.editor.getZoomLevel()
|
||||
for (const overlay of overlays) {
|
||||
const { x, y, radius } = overlay.props
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = 'dodgerblue'
|
||||
ctx.lineWidth = 2 / zoom
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The overlay interface
|
||||
|
||||
Each overlay is a plain object with `id`, `type`, and `props`:
|
||||
|
||||
```tsx
|
||||
interface TLOverlay {
|
||||
id: string // Unique identifier for this instance
|
||||
type: string // Matches the overlay util's static type
|
||||
props: Record<string, unknown> // Data needed for rendering and hit testing
|
||||
}
|
||||
```
|
||||
|
||||
Define a custom interface extending [TLOverlay](?) to type your props. The `id` must be globally unique across all overlay utils — hit testing and hover lookup use the `id` alone. Namespace your ids to avoid collisions: a fixed string like `'highlight'` works for single-instance overlays, while per-item overlays should include the item's identity (e.g. `'handle:<shapeId>:<handleId>'`).
|
||||
|
||||
### Rendering
|
||||
|
||||
The `render()` method receives a `CanvasRenderingContext2D` already transformed to page space (camera offset and zoom applied). Scale line widths and radii by `1 / zoom` to keep them constant on screen:
|
||||
|
||||
```tsx
|
||||
override render(ctx: CanvasRenderingContext2D, overlays: MyOverlay[]): void {
|
||||
const zoom = this.editor.getEfficientZoomLevel()
|
||||
ctx.lineWidth = 1 / zoom
|
||||
// ...draw your overlays
|
||||
}
|
||||
```
|
||||
|
||||
### Hit testing
|
||||
|
||||
By default, overlays are non-interactive. To make an overlay respond to pointer events, implement `getGeometry()` to return a [Geometry2d](?) in page coordinates:
|
||||
|
||||
```tsx
|
||||
import { Circle2d, Geometry2d, TLCursorType } from 'tldraw'
|
||||
|
||||
class MyInteractiveOverlayUtil extends OverlayUtil<MyOverlay> {
|
||||
// ...isActive, getOverlays, render...
|
||||
|
||||
override getGeometry(overlay: MyOverlay): Geometry2d | null {
|
||||
return new Circle2d({
|
||||
x: overlay.props.x - 10,
|
||||
y: overlay.props.y - 10,
|
||||
radius: 10,
|
||||
isFilled: true,
|
||||
})
|
||||
}
|
||||
|
||||
override getCursor(): TLCursorType | undefined {
|
||||
return 'pointer'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
While the select tool is idle, it hit-tests overlays on every pointer move, records the result with [OverlayManager#setHoveredOverlay](?), and applies the cursor from `getCursor()`. Overlays take priority over shapes for hover. To handle clicks yourself, implement `onPointerDown(overlay, info)`; it runs before the default routing, and returning `false` falls through to the default behavior.
|
||||
|
||||
## Registering overlay utils
|
||||
|
||||
Pass your overlay utils through the `overlayUtils` prop on [Tldraw](?) or [TldrawEditor](?):
|
||||
|
||||
```tsx
|
||||
import { Tldraw } from 'tldraw'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw overlayUtils={[HighlightOverlayUtil]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
When using the [Tldraw](?) component, your custom overlay utils are merged with the defaults, so you don't need to re-include them. If your custom overlay util has the same `type` as a default one, it replaces the default. [TldrawEditor](?) has no defaults; pass [defaultOverlayUtils](?) yourself if you want them.
|
||||
|
||||
## Customizing default overlays
|
||||
|
||||
To replace a default overlay, extend it and override the methods you want to change. Give it the same static `type` so it replaces the built-in:
|
||||
|
||||
```tsx
|
||||
import { Tldraw, BrushOverlayUtil, type TLBrushOverlay } from 'tldraw'
|
||||
|
||||
class BlueBrushOverlayUtil extends BrushOverlayUtil {
|
||||
override render(ctx: CanvasRenderingContext2D, overlays: TLBrushOverlay[]): void {
|
||||
const overlay = overlays[0]
|
||||
if (!overlay) return
|
||||
|
||||
const { x, y, w, h } = overlay.props
|
||||
const zoom = this.editor.getZoomLevel()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.rect(x, y, w, h)
|
||||
ctx.fillStyle = 'rgba(0, 0, 255, 0.1)'
|
||||
ctx.fill()
|
||||
ctx.lineWidth = 1 / zoom
|
||||
ctx.strokeStyle = 'blue'
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<Tldraw overlayUtils={[BlueBrushOverlayUtil]} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Options via configure
|
||||
|
||||
Overlay utils can define an `options` property for configuration. Keep `zIndex` in it, since the base type is `{ zIndex?: number }`. Use the static [OverlayUtil#configure](?) method to create a customized version without subclassing:
|
||||
|
||||
```tsx
|
||||
class MyOverlayUtil extends OverlayUtil<MyOverlay> {
|
||||
static override type = 'my_overlay'
|
||||
override options = { zIndex: 50, color: 'red', radius: 10 }
|
||||
|
||||
// ...use this.options.color and this.options.radius in render()
|
||||
}
|
||||
|
||||
// Create a variant with different options
|
||||
const BlueOverlay = MyOverlayUtil.configure({ color: 'blue' })
|
||||
```
|
||||
|
||||
The built-in utils expose their colors and line widths as theme-aware display values, so you can recolor one with `configure({ getCustomDisplayValues })` instead of overriding `render()`. See [BrushOverlayUtilOptions](?) for an example.
|
||||
|
||||
## Accessing overlay utils at runtime
|
||||
|
||||
Use [Editor#overlays](?) to interact with the overlay system:
|
||||
|
||||
```tsx
|
||||
// Get a specific overlay util
|
||||
const brushUtil = editor.overlays.getOverlayUtil<BrushOverlayUtil>('brush')
|
||||
|
||||
// Get all currently active overlays
|
||||
const activeOverlays = editor.overlays.getCurrentOverlays()
|
||||
|
||||
// Hit test at a page point
|
||||
const overlay = editor.overlays.getOverlayAtPoint({ x: 100, y: 200 })
|
||||
|
||||
// Check what's hovered
|
||||
const hoveredId = editor.overlays.getHoveredOverlayId()
|
||||
```
|
||||
|
||||
## Related articles
|
||||
|
||||
- [Shapes](/sdk-features/shapes) - The shape system that overlays interact with
|
||||
- [Indicators](/sdk-features/indicators) - Shape indicators rendered alongside overlays
|
||||
- [Scribble](/sdk-features/scribble) - Scribble system rendered via overlay utils
|
||||
|
||||
## Related examples
|
||||
|
||||
- [Custom overlay](/examples/editor-api/custom-overlay) - Draw a custom canvas overlay on top of the editor.
|
||||
- [Replace a built-in overlay](/examples/editor-api/replace-brush-overlay) - Swap out the brush overlay for a custom implementation.
|
||||
- [Hovered overlay](/examples/editor-api/hovered-overlay) - Read the overlay that the user is hovering.
|
||||
Reference in New Issue
Block a user