FZF for JavaScript
FZF (stylised as fzf) is a command line based fuzzy finder built using Golang. A fuzzy finder allows you to type few characters that appear in a string that you are looking for and get that string from a list of strings quickly. FZF for JavaScript ports FZF's algorithm to JavaScript so that it can be used in the browser. However it doesn't tries to be as performant as its Golang's counterpart, so while this package can work in Node.JS, you are still better off with Golang version for CLI usage.
"FZF for JavaScript" will be referred as "FZF" from here on.
Installation
Node.js
Install FZF by typing the following command in your shell:
npm i fzf
Deno
Grab FZF from a web source:
// use the latestimport { Fzf } from "https://esm.sh/fzf";// or pin it to a versionimport { Fzf } from "https://esm.sh/fzf@0.5.1";// or opt for an alternative sourceimport { Fzf } from "https://cdn.skypack.dev/fzf?dts";
deno.land/x
is not available as a source for now.
Usage
Basic usage
import { Fzf } from "fzf";const list = ["go", "javascript", "python", "rust","swift", "kotlin", "elixir", "java","lisp", "v", "zig", "nim", "rescript","d", "haskell"];const fzf = new Fzf(list);const entries = fzf.find("li");const ranking = entries.map(entry => entry.item).join(", ");console.log(ranking); // Output: lisp, kotlin, elixir
Non-string list
When life is not simple and items in list aren't just plain strings:
const list = [{ id: "1", displayName: "abcd" },{ id: "2", displayName: "bcde" },{ id: "3", displayName: "cdef" },{ id: "4", displayName: "defg" },{ id: "5", displayName: "efgh" },];const fzf = new Fzf(list, {// With selector you tell FZF where it can find// the string that you want to query onselector: (item) => item.displayName,});const entries = fzf.find("cd");const ranking = entries.map((entry) => entry.item.displayName).join(", ");console.log(ranking); // Output: cdef, abcd, bcde
You can also use a combination of fields to search upon:
const fzf = new Fzf(list, {selector: (item) => `${item.displayName} (${item.id})`, // bcde (2)});
Case sensitivity
FZF does smart case searching by default. This means that if the list is
const list = ["Vulpix", "Pikachu", "Bulbasaur", "Typholsion"];
and you query for "pi" you will get Pikachu, Vulpix and Typholsion but if you query for "Pi" you will only get Pikachu back. This can be customized in options.
Highlighting matched characters
Let's take the list above with query "pi". The result will be:
To highlight characters "p" and "i" using React for example, it would be:
const HighlightChars = (props) => {const chars = props.str.split("");const nodes = chars.map((char, i) => {if (props.indices.has(i)) {return <b key={i}>{char}</b>;} else {return char;}});return <>{nodes}</>;};// and can be used like so:const reactElement = <HighlightCharsstr={entry.item.normalize()}indices={entry.positions}/>;
In the last line, entry
is an item from the result list which is returned from fzf.find()
.
normalize()
is a built-in function present on strings. This helps you to receive correct highlight indices. If you are passing a selector function, you would write str={selector(entry.item).normalize()}
.
Find a working example for this and its sourcecode.
Tiebreakers
When we try to match "an" with Thane, Anandnagar, Tripura, Anandpur, Bangalore and Sangam we'll get:
Anandnagar and Anandpur are ranked above the others as their "an" got matched at the very start of those strings. Rest of the three contains "an" somewhere between so they bear the same rank. This ranking is decided by a score
that FZF calculates for each string.
But what if we prefer to have shorter strings appear first? Well here is when a tiebreaker comes in:
import { Fzf, byLengthAsc } from "fzf";// ...const fzf = new Fzf(list, {tiebreakers: [byLengthAsc],});
Now the result will be:
Default (only score) | Score with byLengthAsc tiebreaker |
---|---|
Anandnagar | Anandpur |
Anandpur | Anandnagar |
Thane | Thane |
Bangalore | Sangam |
Sangam | Bangalore |
Note that a tiebreaker is applied only if two items have the same score. That is why Thane, while being the shortest string among the bunch, didn't move to the top.
Implementation detail
A tiebreaker is a function, and just like a sort function it acts on two result entries at a time.
If multiple tiebreakers are given, the first one is applied first. If it is able to rank one entry higher than the other, then this rank is chosen and the next pair of result entries are processed. If not, the next tiebreaker in the list is applied, and so on.
To get an idea on how a custom tiebreaker can be implemented, you can look into the code for built-in ones.
Disabling sorter
FZF doesn't uses a tiebreaker by default. But sometimes you don't want the results to get sorted (by score) either. Thankfully we can turn off the sorting mechanism as well:
const fzf = new Fzf(list, {sort: false,});
When sorting is turned off, tiebreakers won't affect the sorting order either. And so the result items would be returned in the same order as they were in the original input list.
Matching backwards
While matching file paths or links you might prefer for characters that appear the end to be highlighted first. Turning forward=false
just lets you do that.
For example, if you query "comp" you would get:
forward=true (default) | forward=false |
---|---|
src/components/composite-input.ts | src/components/composite-input.ts |
src/components/portal.ts | src/components/portal.ts |
Async finder
Considering other options first
If the list items are many and/or are lengthy, FZF would take more time to process and give you the result back. Usually this time decreases when you type in more letters in the query. However, if you are noticing a minimal lag while typing first few letters, you can employ a faster version for it:
const fzf = new Fzf(list);const fzfFast = new Fzf(list, { fuzzy: "v1" });// ...if (query.length <= 3) {return fzfFast.find(query);} else {return fzf.find(query);}
By default, FZF uses v2 algo which gives better matched positions for items. However, this is something that is not quite noticeable if you have typed very few letters in the query. So v1 can easily replace v2 algo in this case.
Opting for async
If the queries are taking around 100ms to resolve, then you could opt for an async finder as input lags are noticeable if they cross 100ms mark[1]. You can read more about it here.
To use an async finder, we would need to change our code from:
import { Fzf } from "fzf";const fzf = new Fzf(list);const result = fzf.find(query);// process `result`
To:
import { AsyncFzf } from "fzf";const fzf = new AsyncFzf(list);fzf.find(query).then((result) => {// process `result`}).catch(() => {});
For async, FZF cancels previous search before making a new one. This is why we need to put at least an empty function in catch
so that these cancellation errors are handled.
[1] People have different conclusions about this perceivability threshold
Usage with TypeScript
The following code shows a descriptive usage with types. TypeScript can infer these types most of the time for you, so you wouldn't ever need to write them by hand. But for the cases when you do, these are the types you would use:
import { Fzf, FzfResultItem, FzfOptions } from "fzf";interface Fruit {id: string;name: string;}const fruits: Fruit[] = [/* ... */];const options: FzfOptions<Fruit> = {selector: (v) => v.name,};const fzf = new Fzf<Fruit[]>(fruits, options);const results: FzfResultItem<Fruit>[] = fzf.find("ppya"); // gives you back a papaya!
Making it behave like FZF CLI
This library has different defaults compared to FZF CLI. Get a behaviour similar to CLI with these options:
function byTrimmedLengthAsc(a, b, selector) {return selector(a.item).trim().length - selector(b.item).trim().length;}const fzf = new Fzf(list, {match: extendedMatch,tiebreakers: [byTrimmedLengthAsc],});
extendedMatch
is present in the library for you to import and use.
Notes
- If the list is modified after you've initialized FZF using
const fzf = new Fzf(list)
and you want to query usingfzf.find()
after this modification, we strongly suggest you to re-initialize FZF before you make any query. This is suggested because FZF makes some calculations when it is initialized, and these calculations could become outdated because of the modification made.
API
new Fzf(list, options?)

list
can be a list of strings or can have items where in any property of item can resolve to a string.
options
:
limit?
- number, defaults toInfinity
- Iflimit
is 32, top 32 items that matches your query will be returned. By default all matched items are returned.selector?
- function, defaults tov => v
- For each item in the list, target a specific property of the item to search for. This becomes a required option to pass whenlist
is composed of items other than strings.casing?
- string, defaults to"smart-case"
, accepts"smart-case" | "case-sensitive" | "case-insensitive"
- Defines what type of case sensitive search you want.normalize?
- boolean, defaults totrue
- If true, FZF will try to remove diacritics from list items. This is useful if the list contains items with diacritics but you want to query with plain A-Z letters. For example, Zoë will be converted to Zoe.tiebreakers?
- array, defaults to[]
- A list of functions that decide sort order when the score is tied between two result entries. A tiebreaker is nothing but a compare function like the one you find in JS Array sort with an added third argument which isoptions.selector
. First and second arguments are result entries that you receive fromfzf.find
. Note that tiebreakers can't be used ifsort=false
. This library ships with two tiebreakers -byLengthAsc
andbyStartAsc
.sort?
- boolean, defaults totrue
- If true, result items will be sorted in descending order by their score. Otherwise, the items won't be sorted and tiebreakers won't be applied either; they will be returned in the same order that they were in the input list.fuzzy?
- string, defaults to"v2"
, accepts"v1" | "v2" | false
- Select which version of fuzzy algo you want to use. Refer to this explanation to see the differences. If asssignedfalse
, an exact match will be made instead of a fuzzy one.match?
- function, defaults tobasicMatch
- We provide two built-in functions, namelybasicMatch
andextendedMatch
. IfextendedMatch
is used, you can add special patterns to narrow down your search. To read more about how it can be used, see this section. For a quick glance, see this piece.forward?
- boolean, defaults totrue
- If false, items will be matched from backwards. So if we query "/breeds/pyrenees" with "re" it will result in "/breeds/pyrenees" but with forward=false it will result in "/breeds/pyrenees".
fzf.find(query)

For the code below
const entries = new Fzf(list).find(query);const entry = entries[0];
entries
is a list of all the items that were able to match with query
string.
Each entry
contains:
item
- Original item that you send in thelist
.start
- number - Start index where the item is matched.end
- number - End index + 1 where the item is matched.score
- number - Higher the score, closest the item is matched to the query.positions
- A set containing indices of the characters that were matched in the item.