--- 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