Files
upstream-tldraw/apps/docs/content/sdk-features/shape-indexing.mdx
upstream-mirror a774ac673a
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
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

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