Open-source project
rocicorp/fractional-indexing avatar
rocicorp/fractional-indexing

fractional-indexing hands out sort keys that leave room for the next insert

Fractional Indexing in JavaScript

591 stars36 forksJavaScriptCC0-1.0

At a glance

What is it?
rocicorp/fractional-indexing is a CC0-1.0 JavaScript package that generates ordering strings for lists that several people edit at once, so an item can be inserted between two others without rewriting the whole sequence. It is based on David Greenspan's implementation, adds variable length integers and a prepend and append optimization, and the same byte format is implemented in Go, Python, Kotlin and Ruby.
Who is it for?
Adopt fractional-indexing if you maintain an ordered list that more than one client can change, because it gives you keys that sort correctly with ordinary string comparison and leave room for later inserts, and because the Go, Python, Kotlin and Ruby implementations are byte-for-byte compatible so a mixed stack can share one format.
Can I use it commercially?
Yes. CC0-1.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 58 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is two clients inserting into the same list

Fractional indexing is a technique to create an ordering that can be used for realtime editing of ordered sequences, and the reference in the README is Figma's writeup on the problem. The shape of the problem is easy to state. You have a list, and the list is stored as items with a key that determines their order. One client inserts an item at the top, another client inserts an item in the middle, and a third reorders something near the end. If the order is a list of identifiers, or a numeric counter, then any of those operations has to rewrite the order of the items that came after it, which is exactly what makes concurrent editing painful. With a fractional index, the key of an item is a string that encodes its position between its neighbours, and inserting means generating a new key that sorts between the two keys around it. Nothing else changes, so two clients can insert in different places without touching each other's work, and the merge is a sort.

generateKeyBetween is the entire API surface

The package exports two functions, and the first one takes a start key, an end key and returns a key between them:

ts
import { generateKeyBetween } from 'fractional-indexing';

const first = generateKeyBetween(null, null); // "a0"

// Insert after 1st
const second = generateKeyBetween(first, null); // "a1"

// Insert after 2nd
const third = generateKeyBetween(second, null); // "a2"

// Insert before 1st
const zeroth = generateKeyBetween(null, first); // "Zz"

// Insert in between 2nd and 3rd (midpoint)
const secondAndHalf = generateKeyBetween(second, third); // "a1V"

The null arguments are how you say open ended. Passing null for the start means before everything, and passing null for the end means after everything, so the first call in the example creates the first key and the second creates a key after it. The interesting case is the midpoint, where a key between a1 and a2 comes out as a1V, which shows the shape of the scheme: a key that starts with the same prefix as the item it follows, then a digit that places it in between. Keys grow as you insert repeatedly at the same spot, and the library includes a prepend and append optimization so that the common cases of adding to either end do not lengthen the keys unnecessarily.

generateNKeysBetween is the batch version, and it produces shorter keys

The second function generates several keys at a known position, and the README gives the reason to prefer it: it spaces out indexes more evenly and leads to shorter keys. The call takes the same start and end arguments plus a count:

ts
import { generateNKeysBetween } from 'fractional-indexing';

const first = generateNKeysBetween(null, null, 2); // ['a0', 'a1']

// Insert two keys after 2nd
generateNKeysBetween(first[1], null, 2); // ['a2', 'a3']

// Insert two keys before 1st
generateNKeysBetween(null, first[0], 2); // ['Zy', 'Zz']

// Insert two keys in between 1st and 2nd (midpoints)
generateNKeysBetween(second, third, 2); // ['a0G', 'a0V']

The difference from calling generateKeyBetween repeatedly is visible in the results. Two keys placed in a gap in one call come out as a0G and a0V, whereas inserting one at a time and then filling the gap between them produces longer keys with more prefix characters. For a collaborative list where several items are added at once, that difference compounds with every insertion, because the keys are what you store, what you sort, and what you send over the wire.

A custom alphabet has two rules, and both are enforced

Both functions accept an optional digits string that defines the alphabet used for the digit values of a key, defaulting to BASE_62_DIGITS, which is 0-9A-Za-z. You can pass any alphabet, for example base 10:

js
import { generateNKeysBetween } from "fractional-indexing";

generateNKeysBetween(null, null, 4, "0123456789"); // ["50", "51", "52", "53"]

The rules are stated carefully, because the scheme depends on them. First, digits must be single-byte and sorted in ascending character-code order, with no duplicates, and every character must have a char code in the range 0-255, so plain ASCII or Latin-1 but not multi-byte scripts. The generated keys sort correctly using ordinary lexicographic comparison because the digits do. Both rules are validated and throw on violation. Second, the alphabet may be anything that obeys the first rule, and it does not need to include 0-9, A-Z or a-z, so obscure single-byte alphabets such as symbols or Latin-1 work fine as long as they are sorted by character code. Multi-byte alphabets, for example Greek, are rejected.

Integer heads decide whether your keys look classic or self headed

Every key begins with an integer part, and that part starts with a magnitude or length marker, a head, drawn from the intDigits alphabet. The subtlety is that intDigits is split in half, with the first half holding the negative-length heads and the second half the positive-length heads, so it must have an even length. The head only ever occupies the first position and is only compared against other heads, which is why digits and intDigits may overlap or be identical and keys still sort correctly. intDigits defaults to digits, so a custom alphabet is self-headed, and a base 10 key looks like "50", "600" or "49" with no Latin letters. But when you omit digits entirely, intDigits falls back to BASE_52_DIGITS, with A-Z for negative lengths and a-z for positive, so the default keys look like "a0", "b00" or "Z9", the classic format and byte-compatible with the other implementations. Passing digits explicitly, even BASE_62_DIGITS, makes the keys self-headed, and only omitting digits yields the A-Z and a-z heads. If you want a custom digits with the classic Latin heads, or an odd-length digits that cannot supply its own even head alphabet, you pass intDigits explicitly.

Sorting is case-sensitive, and localCompare will get it wrong

The README devotes a section to a failure that is easy to hit and hard to debug. The indexes generated by this library are case-sensitive, and while Array.prototype.sort will work out of the box, String.prototype.localCompare works case-insensitively and will give an incorrect ordering if used as a sort predicate. The fix is to compare the strings natively:

js
const arr = [
  {
    id: "todo_1253241",
    content: "Read the docs",
    fractionalIndex: "YzZ"
  },
  {
    id: "todo_8973942",
    content: "Open a PR",
    fractionalIndex: "Yza" 
  }
]
const sorted = arr.toSorted((a, b) =>
  a.fractionalIndex < b.fractionalIndex
    ? -1
    : a.fractionalIndex > b.fractionalIndex
      ? 1
      : 0,
);

The example data shows why the case sensitivity matters, since YzZ and Yza differ only in the case of the last character, and a case-insensitive comparison would treat them as equal and leave their order up to the sort implementation. The README links the two GitHub issues behind this warning, which is worth reading if your application already sorts strings with a locale aware comparator somewhere in its data layer.

The same format exists in other languages, and jitter is a separate concern

The README lists other implementations that should be byte-for-byte compatible, which is the part that makes the scheme usable in a mixed stack. There is a Go implementation at rocicorp/fracdex, a Python one at httpie/fractional-indexing-python, a Kotlin one at darvelo/fractional-indexing-kotlin and a Ruby one at kazu-2020/fractional_indexer. If your server is written in one of those languages and the client in JavaScript, the keys still sort together, which is the whole point of fixing the format rather than inventing one per stack. A separate concern is collision avoidance. To minimize the likelihood of index collisions when generating fractional indexes concurrently, random jitter can be added to the generated indices, and the README points at a TypeScript package that extends this one with that behaviour, jittered-fractional-indexing by nathanhleung. The distinction matters in review, because jitter changes the key format, and a key with jitter will not interoperate with one that has none.

A tiny package with a public domain licence and fast moving versions

The package is deliberately small. The manifest is a type module with main and exports both pointing at src/index.js, a Node engine range of 14.13.1 or newer, and the published files list is just src/index.js and src/index.d.ts. The build is a declaration emit with tsc followed by a prettier pass over the generated files, the test command runs node src/test.js, and pnpm is the declared package manager. The licence is CC0-1.0, which is a public domain dedication rather than a permissive software licence, so you can take the code into a closed source product without attribution obligations, and that choice suits a library whose value is its format rather than its implementation. The releases came thick and fast at the end. v3.3.0 and v3.4.0 were both published on 2026-06-24, v4.0.0 on 2026-06-25, and the last push was on 2026-08-26, so the major version bump is recent and worth reading about in the changelog before you pin a version, since the head format behaviour differs between the default classic keys and the self headed ones.

Editorial conclusion

Adopt fractional-indexing if you maintain an ordered list that more than one client can change, because it gives you keys that sort correctly with ordinary string comparison and leave room for later inserts, and because the Go, Python, Kotlin and Ruby implementations are byte-for-byte compatible so a mixed stack can share one format. Do not adopt it if your list is edited by one client at a time, since a plain integer or a timestamp would do the job without the extra characters. Before you use it, decide whether your keys will be compared with a locale aware sort, because the README is explicit that localCompare produces the wrong order, and confirm the head format your other services expect, since the default classic format and the self headed format are not interchangeable.

Frequently asked questions

What does the fractional-indexing package generate?

It generates ordering strings for lists that several people edit at once, so an item can be inserted between two others by generating a key that sorts between them rather than by rewriting the order of the items that follow. The two functions are generateKeyBetween for a single key and generateNKeysBetween for several keys at a known position.

How do I sort the fractional indexes correctly?

Use native string comparison. The generated indexes are case-sensitive, and String.prototype.localCompare works case-insensitively and gives an incorrect ordering as a sort predicate, so the README shows a comparison built on the less than and greater than operators, while Array.prototype.sort works out of the box.

Can I use a custom digit alphabet with fractional-indexing?

Yes. Both functions accept an optional digits string, defaulting to BASE_62_DIGITS, which is 0-9A-Za-z. The alphabet must be single-byte and sorted in ascending character code with no duplicates, and every character must have a char code in the range 0-255, so multi-byte scripts are rejected and both rules are validated and throw on violation.

What is the difference between intDigits and digits?

The integer part of every key starts with a head drawn from the intDigits alphabet, which is split in half so the first half holds negative-length heads and the second half positive-length heads, and must therefore have an even length. intDigits defaults to digits, which makes a custom alphabet self-headed, while omitting digits entirely falls back to BASE_52_DIGITS and gives the classic A-Z and a-z heads.

Are there fractional-indexing implementations in other languages?

Yes, and the README says they should be byte-for-byte compatible. They are the Go fracdex, the Python fractional-indexing-python, the Kotlin fractional-indexing-kotlin and the Ruby fractional_indexer, plus a separate TypeScript package that adds random jitter to reduce the chance of collisions when generating indexes concurrently.

Official sources

  1. License: CC0-1.0
  2. Project website
  3. README
  4. Releases
  5. rocicorp/fractional-indexing on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rocicorp-fractional-indexing.svg)](https://hysenlabs.com/projects/rocicorp-fractional-indexing)

Was this useful?