--- 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 `` 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 { 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 // 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::'`). ### 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 { // ...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 (
) } ``` 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 (
) } ``` ### 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 { 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('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.