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
132 lines
5.5 KiB
Plaintext
132 lines
5.5 KiB
Plaintext
---
|
|
title: Shape indexing
|
|
created_at: 12/20/2025
|
|
updated_at: 12/20/2025
|
|
keywords:
|
|
- index
|
|
- z-order
|
|
- fractional
|
|
- sorting
|
|
- layers
|
|
status: published
|
|
date: 12/20/2025
|
|
readability: 8
|
|
voice: 9
|
|
completeness: 9
|
|
accuracy: 10
|
|
notes: 'Minor repetition in collaboration section about jittering.'
|
|
---
|
|
|
|
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](https://www.npmjs.com/package/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.
|
|
|
|
## Related examples
|
|
|
|
- [Layer panel](/examples/ui/layer-panel) - Shows shape hierarchy and z-order in a custom panel
|