fractional-indexing: Implementing Drag-and-Drop Ordering and Avoiding Index Collisions

5 min read
TL;DR
  • With integer indices, reordering often forces you to update neighboring items too, which leads to more write contention in collaborative apps.
  • fractional-indexing generates a sortable string key between two existing keys, so inserting or moving an item usually updates just that one row.
  • Store the key as a string, sort lexicographically, and plan for occasional rebalancing if you operate the list for a long time.
Cover

Avoiding index collisions in sortable lists

The limits of integer indices

If you have ever built a drag-and-drop list, you have probably stored the order like this.

[
  { "id": "a", "order": 1 },
  { "id": "b", "order": 2 },
  { "id": "c", "order": 3 }
]

What happens if you move b to the front? b becomes 0, and a is still 1, so at first glance it seems fine. But if you later want to insert a new item between a and b, you have to shift a to 2 and c to 3. In other words, changing one item often forces you to update several others too.

In collaborative tools where multiple users can reorder items at the same time, that structure tends to create collisions. If two people modify the same part of the list concurrently, the final order can become inconsistent or trigger large update conflicts.

Drag-and-drop UI example with multiple items being reordered

What is fractional-indexing?

David Greenspan introduced this approach in Implementing Fractional Indexing. The core idea is simple: instead of using integers for order, use sortable string keys.

[
  { "id": "a", "order": "a0" },
  { "id": "b", "order": "a1" },
  { "id": "c", "order": "a2" }
]

Want to insert an item between a1 and a2? You can generate a middle key like a1V. Everything else stays unchanged.

Figma uses this idea in its multiplayer editing system. It manages child-node ordering with fractional indexing, which means reordering typically updates only the moved node.

Using the library

In JavaScript, you can use the fractional-indexing package.

npm install fractional-indexing
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';

// First key
const first = generateKeyBetween(null, null);
// → 'a0'

// Insert at the beginning
const zeroth = generateKeyBetween(null, first);
// → 'Zz'

// Insert at the end
const second = generateKeyBetween(first, null);
// → 'a1'

// Generate a key between two existing keys
const third = generateKeyBetween(second, null); // 'a2'
const mid = generateKeyBetween(second, third);
// → 'a1V'

// Generate multiple keys at once
const keys = generateNKeysBetween('a0', 'a2', 2);
// → ['a0G', 'a0V']

You store the key as a string in the database and sort it with lexicographic order using ORDER BY. The scheme is designed so alphabetical order matches the intended item order.

Other ways to manage ordering

fractional-indexing is not the only option. There are a few common alternatives, and each comes with tradeoffs.

Gap strategy with integers

This is the simplest approach. You start with generous spacing.

[
  { "id": "a", "order": 1000 },
  { "id": "b", "order": 2000 },
  { "id": "c", "order": 3000 }
]

To insert between a and b, you assign order: 1500. It is simple and fast. The downside is that once the gaps are exhausted, you eventually need to reindex everything. If inserts keep happening in the same region, you end up with values like 1500 → 1250 → 1375 → ..., and a full rebalance becomes unavoidable.

Illustration of inserting 1.5 between 1 and 2 with the gap strategy

Timestamp-based ordering

Another approach is to use insertion time as the order value.

const item = {
  id: 'a',
  order: Date.now(), // 1700000001000
};

This is the easiest implementation. The problem is that if two clients insert at nearly the same time, ordering becomes ambiguous. For a single-user app, that may be fine. In collaborative environments, it is usually not reliable enough.

Linked list ordering

In this model, each item points to the next item.

[
  { "id": "a", "next": "b" },
  { "id": "b", "next": "c" },
  { "id": "c", "next": null }
]

The nice part is that insertion only touches nearby nodes, so the update scope stays small. The downside is that reading the full order requires traversal, and you lose the convenience of a simple database ORDER BY. If your service reads ordered lists frequently, query complexity can become a real cost.

How to choose

ApproachImplementation complexityCollaboration safetyLong-term operation
fractional-indexingMediumHighRebalancing needed
linked listMediumMediumMore complex queries
integer gapsLowLowReindexing needed
timestampsLowLowCollision risk

If your product involves frequent reordering or multiple users interacting with the same list, fractional-indexing is close to a practical default. For simpler single-user apps, a gap strategy with integers can still be perfectly sufficient.

Things to watch out for

Keys can grow longer over time. If you keep generating new keys inside the same narrow interval, the string length increases. That is why long-running systems often need a rebalancing step that periodically rewrites the ordering keys.

Another important detail is consistency in string comparison. Your database, server, and client should all treat ordering the same way. If different layers compare keys differently, the rendered order can drift from the intended one.

Closing thoughts

If you manage ordering with plain integers, you eventually run into friction. fractional-indexing is a fairly elegant way to avoid that problem. It is especially worth considering when you need realtime collaboration, optimistic updates, or frequent drag-and-drop reordering.

Refs