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
158 lines
5.5 KiB
Plaintext
158 lines
5.5 KiB
Plaintext
---
|
|
title: Indicators
|
|
created_at: 01/31/2026
|
|
updated_at: 01/31/2026
|
|
keywords:
|
|
- indicator
|
|
- selection
|
|
- hover
|
|
- outline
|
|
- ShapeUtil
|
|
- getIndicatorPath
|
|
- hinting
|
|
status: published
|
|
date: 01/31/2026
|
|
---
|
|
|
|
Indicators are the colored outlines that appear around shapes when they're selected, hovered, or being interacted with. Every shape defines its own indicator through the [ShapeUtil#getIndicatorPath](?) method.
|
|
|
|
## How indicators work
|
|
|
|
When you hover over or select a shape, tldraw draws an outline that matches the shape's geometry. The indicator is separate from the shape itself: the [ShapeIndicatorOverlayUtil](?) strokes it on a canvas overlay above the shapes, in the theme's selection color.
|
|
|
|
Indicators appear in three contexts:
|
|
|
|
| Context | When it appears | Stroke weight |
|
|
| -------- | ------------------------------------------------------------------------------------------------- | ------------- |
|
|
| Selected | Shape is in the current selection | 1.5px |
|
|
| Hovered | Pointer is over an unselected shape while the select tool is idle (fine pointers only, not touch) | 1.5px |
|
|
| Hinting | Shape is a drop target during drag operations | 2.5px |
|
|
|
|
For collaborative editing, [CollaboratorShapeIndicatorOverlayUtil](?) also shows which shapes other users have selected, in each collaborator's cursor color. Locked shapes never show indicators.
|
|
|
|
## Defining an indicator
|
|
|
|
Every [ShapeUtil](?) must implement the `getIndicatorPath` method. This method returns a `Path2D`, a richer [TLIndicatorPath](?) object for indicators that need clipping or additional stroked paths, or `undefined` to draw no indicator:
|
|
|
|
```tsx
|
|
import { ShapeUtil, TLShape, Rectangle2d, T, RecordProps } from 'tldraw'
|
|
|
|
declare module 'tldraw' {
|
|
export interface TLGlobalShapePropsMap {
|
|
myshape: { w: number; h: number }
|
|
}
|
|
}
|
|
|
|
type MyShape = TLShape<'myshape'>
|
|
|
|
class MyShapeUtil extends ShapeUtil<MyShape> {
|
|
static override type = 'myshape' as const
|
|
static override props: RecordProps<MyShape> = {
|
|
w: T.number,
|
|
h: T.number,
|
|
}
|
|
|
|
getDefaultProps() {
|
|
return { w: 100, h: 100 }
|
|
}
|
|
|
|
getGeometry(shape: MyShape) {
|
|
return new Rectangle2d({
|
|
width: shape.props.w,
|
|
height: shape.props.h,
|
|
isFilled: true,
|
|
})
|
|
}
|
|
|
|
component(shape: MyShape) {
|
|
return <div style={{ width: shape.props.w, height: shape.props.h }} />
|
|
}
|
|
|
|
getIndicatorPath(shape: MyShape) {
|
|
const path = new Path2D()
|
|
path.rect(0, 0, shape.props.w, shape.props.h)
|
|
return path
|
|
}
|
|
}
|
|
```
|
|
|
|
The `getIndicatorPath` method receives the shape and returns paths in the shape's local coordinate space. You don't need to set stroke color or width—tldraw applies those automatically based on context.
|
|
|
|
### Common indicator patterns
|
|
|
|
For circular shapes, use an ellipse path:
|
|
|
|
```tsx
|
|
getIndicatorPath(shape: MyShape) {
|
|
const { w, h } = shape.props
|
|
const path = new Path2D()
|
|
path.ellipse(w / 2, h / 2, w / 2, h / 2, 0, 0, Math.PI * 2)
|
|
return path
|
|
}
|
|
```
|
|
|
|
For complex paths, use the shape's geometry:
|
|
|
|
```tsx
|
|
getIndicatorPath(shape: MyShape) {
|
|
const geometry = this.editor.getShapeGeometry(shape)
|
|
return new Path2D(geometry.toSimpleSvgPath())
|
|
}
|
|
```
|
|
|
|
### Indicators with labels
|
|
|
|
Shapes with labels may need to clip the indicator where the label appears. Arrow shapes do this to prevent the indicator from overlapping label text. Return an object with `path`, an optional `clipPath`, and optional `additionalPaths`. The clip is applied even-odd before stroking `path`, so an outer rectangle plus the label rectangle punches a hole for the label. `additionalPaths` are stroked afterwards without the clip:
|
|
|
|
```tsx
|
|
override getIndicatorPath(shape: MyShape) {
|
|
const path = new Path2D()
|
|
path.moveTo(0, 0)
|
|
path.lineTo(shape.props.w, shape.props.h)
|
|
|
|
// Even-odd: the outer rect keeps everything, the inner rect punches a hole
|
|
const clipPath = new Path2D()
|
|
clipPath.rect(-100, -100, shape.props.w + 200, shape.props.h + 200)
|
|
clipPath.rect(40, 40, 20, 20)
|
|
|
|
return { path, clipPath }
|
|
}
|
|
```
|
|
|
|
## Hinting shapes
|
|
|
|
Hinting shapes are shapes that receive a highlighted indicator during drag operations. Use [Editor#setHintingShapes](?) to mark shapes as drop targets:
|
|
|
|
```tsx
|
|
// Highlight a shape as a potential drop target
|
|
editor.setHintingShapes([targetShapeId])
|
|
|
|
// Clear hinting
|
|
editor.setHintingShapes([])
|
|
```
|
|
|
|
## Customizing indicators
|
|
|
|
The stroke widths and paint order live on `ShapeIndicatorOverlayUtil.options` (`lineWidth`, `hintedLineWidth`, `zIndex`). Adjust them with `configure`, or subclass the util and override `getOverlays()` to change which shapes get indicators, then pass it through the `overlayUtils` prop. See [Overlay utils](/sdk-features/overlay-utils) for how overlay utils are registered and replaced.
|
|
|
|
```tsx
|
|
import { ShapeIndicatorOverlayUtil, Tldraw } from 'tldraw'
|
|
|
|
const ThickIndicators = ShapeIndicatorOverlayUtil.configure({ lineWidth: 3 })
|
|
|
|
function App() {
|
|
return <Tldraw overlayUtils={[ThickIndicators]} />
|
|
}
|
|
```
|
|
|
|
## Related articles
|
|
|
|
- [Shapes](/sdk-features/shapes) - Learn how to create custom shapes with their own indicators
|
|
- [Selection](/sdk-features/selection) - Understand how selection state controls indicator visibility
|
|
- [Overlay utils](/sdk-features/overlay-utils) - The overlay system that draws indicators
|
|
|
|
## Related examples
|
|
|
|
- [Custom shape](/examples/shapes/tools/custom-shape) - Create a custom shape with an indicator
|
|
- [Custom indicators](/examples/ui/indicators-logic) - Control which shapes show indicators
|