DEV Community

Cover image for fractional-indexing: Implementing Drag-and-Drop Ordering and Avoiding Index Collisions
Kendrick B. Jung
Kendrick B. Jung

Posted on • Originally published at sonim1.com on

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

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 }
]
Enter fullscreen mode Exit fullscreen mode

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" }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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']
Enter fullscreen mode Exit fullscreen mode

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 }
]
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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 }
]
Enter fullscreen mode Exit fullscreen mode

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

Approach Implementation complexity Collaboration safety Long-term operation
fractional-indexing Medium High Rebalancing needed
linked list Medium Medium More complex queries
integer gaps Low Low Reindexing needed
timestamps Low Low Collision 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

Top comments (0)