Back to Tldraw

Shape indexing

apps/docs/content/sdk-features/shape-indexing.mdx

5.4.05.2 KB
Original Source

Every shape in tldraw has an index property that determines its visual stacking order (z-order) on the canvas. Shapes with higher index values appear in front of shapes with lower values. The index system uses string-based fractional indexing, which allows efficient reordering and supports real-time collaboration.

Why fractional indexing?

Integer indices don't leave room to insert. If you have shapes at indices [0, 1, 2] and want to insert between 0 and 1, you'd need to renumber subsequent shapes or use floats that eventually lose precision.

Fractional indexing uses lexicographically sortable strings. You can always generate a new index between any two existing indices. Standard JavaScript string comparison sorts them correctly, and reordering only updates the moved shapes, not every shape on the canvas.

Index structure

Indices are strings like 'a1', 'a2', or 'a1V'. The leading letter says how many characters make up the integer part (a means two characters, b means three, and so on), and anything after that is the fractional part for inserting between existing indices. Digits are base-62. The @tldraw/utils package uses a vendored copy of the jittered fractional indexing algorithm to generate these strings.

Generating indices

The @tldraw/utils package exports functions for generating indices:

ts
import {
	getIndexBetween,
	getIndexAbove,
	getIndexBelow,
	getIndicesAbove,
	getIndicesBelow,
	getIndicesBetween,
	sortByIndex,
	IndexKey,
} from '@tldraw/utils'

// Generate a single index between two existing indices
const between = getIndexBetween('a1' as IndexKey, 'a3' as IndexKey) // e.g. 'a2'

// Generate indices above or below an existing index
const above = getIndexAbove('a1' as IndexKey) // e.g. 'a2'
const below = getIndexBelow('a2' as IndexKey) // e.g. 'a1'

// Generate multiple indices at once
const multipleAbove = getIndicesAbove('a0' as IndexKey, 3)
const multipleBelow = getIndicesBelow('a2' as IndexKey, 2)
const multipleBetween = getIndicesBetween('a0' as IndexKey, 'a2' as IndexKey, 2)

// Sort objects by their index property
const shapes = [{ index: 'a2' as IndexKey }, { index: 'a1' as IndexKey }]
shapes.sort(sortByIndex) // [{ index: 'a1' }, { index: 'a2' }]

The algorithm jitters (randomizes) the generated keys, so the exact values above vary between calls. This reduces conflicts in collaborative environments: when two users insert shapes at the same position simultaneously, they generate different indices instead of identical ones, and both operations merge cleanly. Jittering is disabled when NODE_ENV is test so tests are deterministic.

getIndices(n, start?) returns the starting index (default 'a1') followed by n indices above it. ZERO_INDEX_KEY is the first index, 'a0'. To find the next free index in a parent, use Editor#getHighestIndexForParent with getIndexAbove.

Reordering shapes

The Editor provides four methods for changing shape z-order. You can pass either shape IDs or shape objects to any of these methods.

Send to back and bring to front

ts
// Move shapes to the very back or front
editor.sendToBack(['shape:abc123' as TLShapeId, 'shape:def456' as TLShapeId])
editor.bringToFront(['shape:abc123' as TLShapeId])

These move shapes to the bottom or top of the z-order within their parent.

Send backward and bring forward

ts
// Move shapes one step back or forward
editor.sendBackward(['shape:abc123' as TLShapeId])
editor.bringForward(['shape:abc123' as TLShapeId])

By default, these methods only move shapes past other shapes whose page bounds overlap theirs. This makes keyboard shortcuts feel intuitive: pressing "send backward" moves a shape behind the shape it's actually covering, not behind some distant shape.

To move past any shape regardless of overlap:

ts
editor.sendBackward(['shape:abc123' as TLShapeId], { considerAllShapes: true })

Order preservation

All reordering methods preserve relative order. If you select shapes A, B, and C (stacked A-B-C from back to front) and bring them forward, they stay in A-B-C order at their new position.

How reordering works internally

Shape indices are always relative to siblings within the same parent. When you reorder shapes, tldraw:

  1. Groups shapes by parent
  2. Finds the insertion point (front, back, or adjacent to an overlapping shape)
  3. Generates new indices using getIndicesBetween
  4. Updates only the shapes that actually need new indices

If shapes are already at the target position, no updates occur.

If you need full control over ordering (for example in a layer panel), set a shape's index directly with Editor#updateShapes.

Index validation

IndexKey is a branded type, so you can't accidentally pass an arbitrary string as an index. To validate a string at runtime, use the indexKey validator from @tldraw/validate:

ts
import { T } from 'tldraw'

T.indexKey.validate('a1') // returns 'a1' as IndexKey
T.indexKey.validate('invalid!') // throws a ValidationError

The store validates indices when you create or update shapes, so the editor won't enter an invalid state.

  • Layer panel - Shows shape hierarchy and z-order in a custom panel