# match-sorter: Deterministic Best-Match Sorting for JavaScript Lists

> match-sorter ranks array items by fixed match tiers instead of fuzzy scores, so autocomplete results stay stable as the user types. This review covers the install, the ranking rules, and the cases where it is the wrong tool.

**kentcdodds/match-sorter** — Simple, expected, and deterministic best-match sorting of an array in JavaScript

- Repository: https://github.com/kentcdodds/match-sorter
- Website: https://npm.im/match-sorter
- Stars: 4,108 · Forks: 143
- Language: TypeScript
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/kentcdodds-match-sorter

## The filtering problem match-sorter is built to solve

You have a list. Dozens of items, maybe thousands. There is a filter input above it, and the user types one character at a time. The naive approach, lowercasing both sides and calling indexOf, breaks down immediately: with the query "h" every item containing an h anywhere ranks equally, so the list looks random. Reaching for a fuzzy string library introduces the opposite problem. Scores shift as the query grows, items jump between positions between keystrokes, and the user loses track of what they were looking at.

match-sorter takes the position that ordering should be fixed and explainable. The README frames the goal as "simple, expected, and deterministic sorting of the items (no fancy math algorithm that fancily changes the sorting as they type)". The audience is anyone building an autocomplete, a command palette, a table filter, or a dropdown with a search box, where the list is small enough to sort on every keystroke and the user is watching the order change in real time. It is not aimed at search over documents or long prose, and the ranking rules make that clear.

## How the ranking tiers work

The algorithm assigns each item to one of seven ordered tiers, and the tier decides the position. The README lists them with country examples. Case-sensitive equality comes first: "France" matches "France" but not "france". Then case-insensitive equality. Then starts-with, so "Sou" matches "South Korea" and "South Africa". Then word-starts-with, which is what lets "Repub" match "Dominican Republic". Then contains, so "ham" matches "Bahamas". Then acronym, so "us" matches "United States". Last is simple match, where the query letters appear in the same order somewhere in the item: "iw" matches "Zimbabwe" but not "Kuwait", because the order is wrong.

The simple-match tier is the only one with internal ranking. The README states that a closer match ranks higher, and gives the example that "ua" matches "Uruguay" more closely than "United States of America", so Uruguay is ordered first. That is the whole scoring model. There is no edit distance, no term frequency, no weighting by field. Every item lands in exactly one tier, and ties inside a tier fall back to the original array order unless you supply a baseSort.

That design is the reason results do not shuffle. Adding a character to the query can move an item to a different tier, but it cannot produce a slightly different score that reshuffles items within a tier. The trade-off is that match-sorter has no notion of relevance beyond the tier ladder. An item that is a strong semantic match but poor character match will rank below an item that happens to start with the query string.

## Installing match-sorter and running a first query

The package is published on npm and the README says to install it as a project dependency. The package.json lists a single runtime dependency, remove-accents, plus @babel/runtime, so the install is small.

```bash
npm install match-sorter
```

Import the named export and pass an array plus a query. The README gives this exact example, and the returned array is the filtered, ordered result:

```javascript
import {matchSorter} from 'match-sorter'
// or const {matchSorter} = require('match-sorter')
// or window.matchSorter.matchSorter
const list = ['hi', 'hey', 'hello', 'sup', 'yo']
matchSorter(list, 'h') // ['hello', 'hey', 'hi']
matchSorter(list, 'y') // ['yo', 'hey']
matchSorter(list, 'z') // []
```

Note that "hello" sorts before "hey" and "hi" for the query "h". All three are starts-with matches, so the order comes from the simple-match tiebreak inside that tier, not from alphabetical order. If you want to see why an item landed where it did, the library exports a second function that returns the ranking metadata instead of plain items:

```javascript
import {matchSorterWithRankInfo} from 'match-sorter'

const rankedResults = matchSorterWithRankInfo(list, 'h')
// [
//   {
//     item: 'hello',
//     rankedValue: 'hello',
//     rank: 5,
//     keyIndex: -1,
//     keyThreshold: undefined,
//     index: 2,
//   },
//   // ...
// ]
```

The rank field is the tier, and index is the item's position in the input array. That output is the fastest way to confirm the tier ladder behaves the way you expect on your own data before you wire it into a UI.

## Keys, nested objects and array fields

Most real lists are arrays of objects, not strings. The keys option tells match-sorter which fields to rank, and the order of that array matters: the README's example with `{keys: ['name', 'color']}` puts George first for the query "g" because his name starts with the letter, while Janice and Fred match only on color. Swapping to `{keys: ['color', 'name']}` changes the order, because the first key is evaluated first.

Nested access uses dot notation. `keys: ['name.first']` reaches into a nested object, and `keys: ['name.0.first']` reaches into the first element of a nested array. The README is explicit that bracket syntax does not work: `matchSorter(nestedObjList, 'j', {keys: ['name[0].first']})` is documented as failing. For arrays of unknown length, a `*` wildcard stands in for the index, so `keys: ['aliases.*.name.first']` searches every alias object.

When a key holds an array of values, the best match from that array is used for ranking. The README's ice cream example shows `matchSorter(iceCreamYum, 'cc', {keys: ['favoriteIceCream']})` returning the entry whose array contains "candy cane" ahead of the one containing "mint, chocolate". This is the mechanism behind tag and alias fields, and it saves you from flattening those arrays before filtering.

## Where match-sorter is the wrong tool

The ranking ladder has no concept of partial credit across fields. If an item matches weakly on three keys and another matches strongly on one, the tier system decides, not a combined score. For a small picker that is fine. For a search box over product descriptions, support tickets, or documentation, it is not: the contains tier will pull in anything with the substring, and the simple-match tier will pull in items where the query letters happen to appear in order across a long string. Long text produces noisy simple matches, and match-sorter has no way to weight them down.

There is also a hard dependency on sorting the full array on every query. The README's framing is dozens, hundreds, or thousands of items. Nothing in the package.json or the README suggests an index, a precomputed structure, or an incremental update path. If your list is large enough that a full sort per keystroke is noticeable, you need a different approach, either a real search index or a debounce plus a smaller candidate set.

The diacritics handling is another boundary. The package depends on remove-accents, and the advanced options list a keepDiacritics boolean, which implies accents are stripped by default. That is usually what you want for a name picker, but it means you cannot rely on match-sorter to distinguish accented forms without opting out, and the README does not document what the ranking looks like when keepDiacritics is true.

## match-sorter compared with fuzzy matching libraries

The obvious alternative is a fuzzy string scoring library such as Fuse.js or fuse-style scorers, which compute a numeric distance between the query and each candidate and sort by that number. The difference in approach is the source of the ordering. A fuzzy scorer produces a continuous score, so a one-character change in the query can reorder the entire result set, and the ordering is hard to explain to a user who asks why one item beat another. match-sorter produces a discrete tier, so the ordering is a lookup in a seven-row table that you can read off the README.

That makes match-sorter easier to reason about and easier to test, since a snapshot of the output array is stable across versions in a way a score threshold is not. Fuzzy scorers do better on the case match-sorter handles worst: a query that is a typo, a transposition, or a subsequence spread across a long string. match-sorter's simple-match tier requires the letters in order and gives no credit for a swapped pair, so "Gmec" will not find "George" the way an edit-distance scorer would.

A second alternative is doing nothing and using a plain filter plus localeCompare. That is faster and has zero dependencies, but it loses the tier ordering entirely, which is the feature you came for.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-05-13. Recent releases are v8.3.0 on 2026-04-15, v8.2.0 on 2025-11-24, and v8.1.0 on 2025-07-24, so the release cadence is roughly a minor version every few months. The version field in package.json is the placeholder 0.0.0-semantically-released, which means releases are cut by the semantic-release pipeline rather than hand-edited, so the changelog on the release page is the authoritative record of what changed.

The licence is MIT, which permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is a permissive licence with no copyleft obligation on your own code. This is a description of the licence text, not legal advice; if your organisation has a policy on third-party dependencies, route it through that process.

The upgrade surface is small. The public API is two functions, matchSorter and matchSorterWithRankInfo, plus the options object. The dependency list is two runtime packages. Major-version bumps in the 8.x line are the ones to read carefully, since a change to the tier ladder would change output order without changing your call sites. The build output is shipped as CommonJS, ESM and a TypeScript declaration file, so consumers on any of those module systems get a build without configuring anything.

## Conclusion

Adopt match-sorter when you need predictable, explainable ordering of a few thousand items behind a filter input, and when you want the ranking metadata from matchSorterWithRankInfo for debugging. Do not adopt it as a general relevance engine for documents, or if you need scored fuzzy matching that degrades gracefully on long text. Before committing, run matchSorter over your real data with a few partial queries and confirm the tier order matches what your users expect.

## FAQ

### What is match-sorter used for?

It filters and sorts an array of items based on a query string, ranking results by a fixed set of match tiers such as case-sensitive equality, starts-with and contains. The README frames it as the tool for a list of dozens to thousands of items behind a filter input where you want deterministic ordering.

### How do I install match-sorter?

The README says to install it as a project dependency with npm install match-sorter, then import the named export matchSorter from the package.

### Does match-sorter work with arrays of objects instead of strings?

Yes. Passing a keys array tells match-sorter which fields to rank, and the order of that array determines which key is evaluated first. Nested fields use dot notation such as name.first, and array fields rank by their best matching value.

### Can I see why match-sorter ranked an item where it did?

The package exports matchSorterWithRankInfo, which returns objects containing the item, its rank tier, the keyIndex, and the item's index in the original array. That output shows which tier produced the position.

### Does match-sorter handle typos or misspelled queries?

Not in the way an edit-distance scorer would. The lowest tier, simple match, requires the query letters to appear in the same order in the item, so a transposed or misspelled query will not match unless the letters still line up in sequence.

## Sources

- [kentcdodds/match-sorter on GitHub](https://github.com/kentcdodds/match-sorter)
- [License: MIT](https://github.com/kentcdodds/match-sorter/blob/main/LICENSE)
- [Project website](https://npm.im/match-sorter)
- [README](https://github.com/kentcdodds/match-sorter/blob/main/README.md)
- [Releases](https://github.com/kentcdodds/match-sorter/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/kentcdodds-match-sorter
