snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
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
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
This commit is contained in:
131
apps/docs/content/sdk-features/shape-indexing.mdx
Normal file
131
apps/docs/content/sdk-features/shape-indexing.mdx
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user