Type "javscript paterns" into most search boxes and you get nothing. That single transposed letter is enough to make a strict includes() filter come up empty, even though the human staring at the screen knows exactly what they meant. Fuse.js exists to close that gap. It is a lightweight, zero-dependency fuzzy-search library written in TypeScript that runs in the browser and on the server, and it specializes in finding the right result even when the query is misspelled, partial, or in the wrong order.
The pitch is refreshingly modest: you have a small-to-medium dataset already loaded on the client (a list of products, docs pages, contacts, command-palette actions), and you want forgiving search over it without standing up Elasticsearch, Algolia, or Meilisearch. Fuse.js does that in roughly 9 kB gzipped, with no dependencies, and it has been quietly powering autocomplete boxes, command palettes, and filterable tables for years. With around 11 million weekly downloads it is one of the most-used search libraries on npm, and version 7.4 added two genuinely big upgrades: multi-word token search with relevance ranking, and parallel search across Web Workers.
Why Reach for Fuse.js
The core engine uses the Bitap algorithm for approximate string matching, which is a fancy way of saying it tolerates typos, transpositions, and partial matches out of the box. You do not configure that behavior; it is the default. On top of that foundation, Fuse.js layers a handful of capabilities that make it feel less like a string matcher and more like a small search engine:
- Weighted keys so a match in a title can rank higher than a match in a description.
- Match highlighting that returns character-level indices, so you can bold the exact letters that matched.
- Extended search operators for exact matches, prefixes, suffixes, and negation when you want precision.
- Token search (new in 7.4) for multi-word queries with BM25-style relevance ranking.
- Logical search with
$andand$orfor compound conditions. - Dynamic collections so you can add and remove documents from a live index without rebuilding it.
- Parallel search via the
FuseWorkerclass for large datasets that would otherwise block the main thread.
All of that, and it stays tiny. The full build is about 8.6 kB minified and gzipped; if you only need plain fuzzy search there is a basic build at around 6.8 kB. No runtime dependencies, Apache-2.0 licensed, and shipped with proper TypeScript declarations for both ESM and CommonJS consumers.
Getting It Into Your Project
Installation is a single command, with no peer dependencies to chase down.
npm install fuse.js
yarn add fuse.js
If you would rather skip the bundler entirely, there is a CDN build you can drop into a <script type="module"> tag, but for any real React or Node project you will want the npm package so your tooling can tree-shake and type-check it.
Your First Forgiving Search
The mental model is simple: hand Fuse.js a list of objects, tell it which keys to look at, and call search. Here is a small catalog of books being searched with a deliberate typo.
import Fuse from 'fuse.js'
interface Book {
title: string
author: string
}
const books: Book[] = [
{ title: "Old Man's War", author: 'John Scalzi' },
{ title: 'The Lock Artist', author: 'Steve Hamilton' },
{ title: 'HTML5', author: 'Remy Sharp' },
{ title: 'JavaScript: The Good Parts', author: 'Douglas Crockford' },
]
const fuse = new Fuse(books, {
keys: ['title', 'author'],
})
const results = fuse.search('javscript')
// → [{ item: { title: 'JavaScript: The Good Parts', ... }, refIndex: 3, score: 0.02 }]
Each result carries the matched item, its refIndex in the original array, and a score where lower means a closer match. Notice we never told Fuse.js what "javscript" was supposed to be; the Bitap engine figured out that the missing letter was close enough. You can also index a plain array of strings if your data is not made of objects, in which case you skip the keys option entirely.
Ranking Some Fields Above Others
In real apps not every field is equally important. A hit in a title usually matters more than a hit buried in a long description. Fuse.js lets you express that with weighted keys, where each key gets a relative importance value.
const fuse = new Fuse(docs, {
keys: [
{ name: 'title', weight: 2 },
{ name: 'description', weight: 1 },
],
})
Now a query that matches a title scores roughly twice as strongly as the same query matching only a description, so the most relevant items float to the top. You can also point at nested fields using dotted paths like 'author.name', which keeps the configuration readable even for deeply structured records.
Highlighting What Actually Matched
A search box feels far more responsive when the matched characters are visibly emphasized. Turn on includeMatches and Fuse.js hands you the exact index ranges that matched, ready to wrap in <mark> tags.
const fuse = new Fuse(books, {
includeMatches: true,
keys: ['title'],
})
const [first] = fuse.search('javscript')
// first.matches[0].indices → [[0, 9]]
// first.matches[0].key → 'title'
Version 7.4 tightened this behavior so highlight ranges stay inside the actual matched window instead of scattering stray characters across the string, which used to be a long-standing annoyance. One small behavior change to note if you are upgrading: match.key is now always a dotted string, even for keys you declared as path arrays, so highlight code that expected an array shape needs a quick update.
Pushing It Further
Speaking the Operator Language
Sometimes fuzzy is too fuzzy and you want surgical control. Enable useExtendedSearch and your query string gains a small operator vocabulary: = for an exact match, ^ for a prefix, ! for negation, and a quote prefix to force a literal include match.
const fuse = new Fuse(list, {
useExtendedSearch: true,
keys: ['title'],
})
fuse.search('=exact match') // only exact matches
fuse.search('^prefix') // titles starting with "prefix"
fuse.search('!draft') // exclude anything containing "draft"
This is the escape hatch for power users and for building filter UIs where you want predictable, rule-based behavior alongside the forgiving default. For compound conditions there is also logical search, where you pass an object built from $and and $or arrays to require or combine matches across specific keys.
Multi-Word Search That Ranks by Relevance
The headline feature of version 7.4 is token search, switched on with useTokenSearch: true. Instead of treating your query as one fuzzy blob, it splits the input into individual words, fuzzy-matches each one independently, and ranks results using BM25-style IDF weighting, which means rarer words count for more than common ones.
const fuse = new Fuse(articles, {
useTokenSearch: true,
keys: ['title', 'body'],
})
fuse.search('express midleware rout')
// Finds "Express Middleware" and "Express Routing Guide" despite the typos
Token search is word-order independent, so "patterns javascript" and "javascript javascript" return the same set, and it has no practical query-length limit because each word is searched on its own. By default any matching word is enough to surface a record, but setting tokenMatch: 'all' flips it into filter mode where every word must appear somewhere in the record, so adding words narrows the results. The default tokenizer is Unicode-aware and segments scripts like CJK, Cyrillic, Greek, and Arabic automatically, and you can supply your own tokenize regex or function for tricky tokens like node.js or c++. Token search lives in the full build, and note that it is not supported by the one-off Fuse.match() helper, since it needs corpus-wide statistics that a single-string comparison cannot provide.
Searching Big Lists Without Freezing the UI
Fuzzy search over tens of thousands of records on the main thread can make typing feel sticky. Version 7.4 introduced the FuseWorker class, imported from the fuse.js/worker entry point, which shards your collection across several Web Workers and searches them in parallel. The API mirrors Fuse almost exactly, except search is now asynchronous, and the maintainers measured roughly a 5x speedup on 100,000 documents.
import { FuseWorker } from 'fuse.js/worker'
const fuse = new FuseWorker(documents, {
keys: ['title', 'author', 'description'],
})
const results = await fuse.search('query')
fuse.terminate() // free the workers when you are done
Result ordering matches single-threaded Fuse, so you can switch a large search over to workers without rethinking your UI. The one constraint worth remembering is that anything that cannot be cloned and sent to a worker is off the table: function-valued options like sortFn and getFn are not supported, and useTokenSearch is rejected inside a worker. For everything else, this is the cleanest way to keep scrolling and typing smooth while a heavy index does its work in the background.
Keeping the Index in Sync With React
Because Fuse.js is a plain library with no React bindings, the idiomatic pattern is to memoize the instance so you are not rebuilding the index on every render. Wrap construction in useMemo keyed on your data and options, then run searches against the stable instance, ideally behind a debounce so you are not querying on every keystroke.
import { useMemo, useState } from 'react'
import Fuse from 'fuse.js'
function useBookSearch(books: Book[]) {
const fuse = useMemo(
() => new Fuse(books, { keys: ['title', 'author'] }),
[books],
)
const [query, setQuery] = useState('')
const results = query ? fuse.search(query) : []
return { query, setQuery, results }
}
For live data you do not have to throw the index away and rebuild it either. The add and remove methods mutate the existing index in place, and 7.4 fixed the searcher cache so it invalidates correctly after those mutations.
Worth Keeping in Your Toolbox
Fuse.js occupies a sweet spot that bigger search engines tend to miss. When you want typo-tolerant, multi-word search over data you already have on the client, with near-zero setup, no dependencies, and a bundle small enough that nobody will complain, it is hard to beat. Heavier in-browser engines like MiniSearch and FlexSearch make sense once you need a true inverted-index full-text engine or maximum throughput on very large corpora, and a hosted service like Algolia is the answer when search becomes a core product surface. But for the everyday case of a command palette, an autocomplete, or a filterable list, Fuse.js gives you forgiving search that just works.
What makes the current 7.4 line interesting is that it stretches that sweet spot upward. Token search brings relevance-ranked, multi-word queries that used to push people toward MiniSearch, and FuseWorker lets you take that forgiving search to datasets large enough that the main thread would otherwise stutter. It is still the same tiny, zero-dependency library, actively maintained and shipping regular releases, just with a notably longer reach than it had a version ago. If you have been hand-rolling filter and includes for your search box, this is the upgrade worth making.