Sign In

ink-combobox

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ink-combobox

Combobox/fuzzy-search autocomplete input for Ink

latest
Source
npmnpm
Version
0.2.2
Version published
Maintainers
1
Created
Source

ink-combobox

npm version CI license npm downloads

ink-combobox demo

Note: The npm package name is ink-combobox. Install with npm install ink-combobox.

A combobox/fuzzy-search autocomplete input component for Ink. Provides a fully interactive dropdown with fuzzy matching, keyboard navigation, async data loading, and scroll indicators -- all inside your terminal.

Features

  • Fuzzy matching with highlighting -- matched characters are highlighted in the dropdown so users can see why each result was returned
  • Keyboard navigation -- arrow keys, enter to select, tab to autofill, escape to close
  • Async options -- pass a function that returns a promise to load options from an API or database
  • Error handling -- async failures surface via onError callback and show an error message in the dropdown
  • Debounce -- configurable debounce for both sync and async filtering
  • Scroll indicators -- when the list overflows the visible window, arrow indicators show how many items are above/below
  • Cursor movement -- full input editing with left/right arrows, home/end, and forward/backward delete
  • Headless hooks -- use the exported hooks to build your own custom UI on top of the autocomplete logic
  • Themeable -- swap out the default chalk-based theme to match your CLI's style

Install

npm install ink-combobox

Peer dependencies: ink >= 6.0.0, react >= 19.0.0, and chalk >= 5.0.0. Requires Node.js >= 20.

Note: chalk is a peer dependency because Ink bundles it internally. Keeping it as a peer avoids version conflicts. If your project already depends on Ink, chalk is likely already available.

Quick Start

import React from 'react';
import { render } from 'ink';
import { Autocomplete } from 'ink-combobox';

const fruits = [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
  { label: 'Cherry', value: 'cherry' },
  { label: 'Grape', value: 'grape' },
  { label: 'Strawberry', value: 'strawberry' },
];

function App() {
  return (
    <Autocomplete
      options={fruits}
      placeholder="Search fruits..."
      onSelect={(value) => console.log('Selected:', value)}
    />
  );
}

render(<App />);

Props

The <Autocomplete> component accepts the following props:

PropTypeDefaultDescription
optionsOption[] | AsyncOptionsProvider(required)Static array of options or an async function (query: string) => Promise<Option[]>
placeholderstring''Placeholder text shown when input is empty
defaultValuestring''Initial input value (the input starts pre-filled with this string)
visibleOptionCountnumber5Maximum number of options visible in the dropdown at once
debounceMsnumber0 (sync) / 150 (async)Milliseconds to debounce filtering. Defaults to 150ms for async providers
isDisabledbooleanfalseWhen true, the input ignores all keyboard input
prefixstring'> 'Text displayed before the input
noMatchesTextstring'No matches'Text shown when no options match the current query
loadingTextstring'Loading...'Text shown while an async provider is fetching results
errorTextstringundefinedText shown when an async provider rejects. If not set, the error's .message is displayed
onChange(value: string) => void--Called whenever the input value changes
onSelect(value: string, option?: Option) => void--Called when the user selects an option (Enter key); option is the full selected Option
onError(error: Error) => void--Called when an async options provider rejects. The error is also exposed in state as error

Each Option has the shape { label: string; value: string }.

Keyboard Shortcuts

KeyAction
Any characterAppends to the input and opens the dropdown
BackspaceDeletes the character before the cursor
DeleteDeletes the character before the cursor (macOS sends 0x7F for the physical delete key)
Ctrl+DDeletes the character after the cursor (forward delete)
Left ArrowMoves cursor left
Right ArrowMoves cursor right
Ctrl+AMoves cursor to start of input
Ctrl+EMoves cursor to end of input
Down ArrowMoves focus to the next option
Up ArrowMoves focus to the previous option
EnterSelects the focused option, shows its label in the input, and closes the dropdown
TabFills the input with the focused option's label (keeps dropdown open)
EscapeCloses the dropdown and clears the input

Async Options

Pass a function instead of an array to load options dynamically. The function receives the current query string and should return a promise that resolves to an array of Option objects. The provider is responsible for its own filtering -- the component will not re-filter the results, but it will compute match highlight ranges for display.

import { Autocomplete } from 'ink-combobox';

async function searchUsers(query: string) {
  const response = await fetch(`/api/users?q=${encodeURIComponent(query)}`);
  const users = await response.json();
  return users.map((u) => ({ label: u.name, value: u.id }));
}

function App() {
  return (
    <Autocomplete
      options={searchUsers}
      placeholder="Search users..."
      debounceMs={200}
      loadingText="Searching..."
      errorText="Failed to load users"
      onError={(err) => console.error('Search failed:', err)}
      onSelect={(userId) => console.log('Selected user:', userId)}
    />
  );
}

When the async provider rejects, the component displays the error message (or the errorText prop if provided) in place of the "No matches" text. The onError callback fires so you can log or handle the failure. Typing new input clears the error and retries.

Fuzzy Matching

The built-in fuzzy matcher uses a two-pass approach (similar to fzf v1) for better match alignment:

  • Forward pass -- greedy left-to-right scan to confirm a match exists and find initial character positions
  • Backward pass -- from the last matched position, scans backward to find a tighter alignment with more consecutive characters

Both passes are scored and the better result is used. This is still O(n) per candidate but produces much better alignments. For example, matching "ab" against "xaxxab" finds the consecutive [4,5] alignment rather than the greedy [1,4].

Scoring factors:

  • Consecutive character bonus -- characters matched in a row score higher
  • Word boundary bonus -- matches at the start of words (after spaces, hyphens, underscores, slashes, dots) get a boost
  • CamelCase boundary bonus -- matches at camelCase transitions (e.g., the N in getName) score higher
  • First character multiplier -- the first matched character gets a 2x score multiplier
  • Gap penalty -- gaps between matched characters reduce the score

Scores are normalized to a 0-1 range. Results are sorted by score descending, so the best matches appear first.

Standalone Usage

The fuzzy matching utilities are exported for use outside the component:

import { fuzzyMatch, fuzzyFilter, collapseIndices } from 'ink-combobox';

// Match a single query against a label
const result = fuzzyMatch('gn', 'getName');
// => { score: 0.39, matchedIndices: [0, 3] }

// Convert matched indices to contiguous ranges (for highlighting)
const ranges = collapseIndices(result.matchedIndices);
// => [{ start: 0, end: 1 }, { start: 3, end: 4 }]

// Filter and sort an array of options
const options = [
  { label: 'getName', value: 'getName' },
  { label: 'setName', value: 'setName' },
  { label: 'getAge', value: 'getAge' },
];
const matches = fuzzyFilter('gn', options);
// => sorted array of { option, score, matchRanges }

Scroll Indicators

When the number of matching options exceeds visibleOptionCount, the dropdown shows scroll indicators:

> app
  ❯ Apple
    Application
    Pineapple
  ↓ 2 more

Scrolling down reveals more items and shows an upward indicator:

> app
  ↑ 1 more
    Application
  ❯ Pineapple
  ↓ 1 more

Headless Usage

For full control over rendering, use the exported hooks directly instead of the <Autocomplete> component:

import React, { useMemo } from 'react';
import { Text, Box } from 'ink';
import {
  useAutocompleteState,
  useAutocomplete,
  AutocompleteOption,
} from 'ink-combobox';

function CustomAutocomplete() {
  const options = useMemo(() => [
    { label: 'Red', value: 'red' },
    { label: 'Green', value: 'green' },
    { label: 'Blue', value: 'blue' },
  ], []);

  const { state, dispatch } = useAutocompleteState({
    options,
    visibleOptionCount: 10,
    onSelect: (value) => console.log('Picked:', value),
  });

  const { renderedInput } = useAutocomplete({
    state,
    dispatch,
    placeholder: 'Pick a color...',
  });

  return (
    <Box flexDirection="column">
      <Text>{renderedInput}</Text>
      {state.isOpen && state.filteredOptions.map((match, i) => (
        <AutocompleteOption
          key={match.option.value}
          label={match.option.label}
          matchRanges={match.matchRanges}
          isFocused={i === state.focusedIndex}
        />
      ))}
    </Box>
  );
}

useAutocompleteState(options)

Manages the reducer state for the autocomplete: input value, cursor position, filtered options, focused index, scroll window, loading state, error state, and selection. Returns { state, dispatch }.

useAutocomplete({ state, dispatch, isDisabled?, placeholder? })

Binds keyboard input handling via Ink's useInput and builds the rendered input string with cursor visualization. Returns { renderedInput }.

Performance Tips

Memoize large option arrays. When passing a static options array, wrap it with useMemo (or define it outside the component) to avoid creating a new array reference on every render. The component uses a JSON.stringify shallow comparison as a safety guard against inline array literals, but this runs on every render cycle. For large arrays, always memoize to avoid unnecessary serialization:

// Good: stable reference, no serialization overhead
const options = useMemo(() => [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
], []);

// Good: defined outside the component
const OPTIONS = [
  { label: 'Apple', value: 'apple' },
  { label: 'Banana', value: 'banana' },
];

// Avoid: new array every render triggers JSON.stringify comparison
<Autocomplete options={[{ label: 'Apple', value: 'apple' }]} />

TypeScript

All types are exported from the package:

import type {
  Option,              // { label: string; value: string }
  MatchRange,          // { start: number; end: number }
  FuzzyMatch,          // { option: Option; score: number; matchRanges: MatchRange[] }
  AsyncOptionsProvider,// (query: string) => Promise<Option[]>
  OptionsSource,       // Option[] | AsyncOptionsProvider

  AutocompleteProps,
  AutocompleteState,
  AutocompleteAction,
  AutocompleteOptionProps,
  UseAutocompleteOptions,
  UseAutocompleteStateOptions,
  Theme,
} from 'ink-combobox';

Contributing

Contributions are welcome. Please open an issue to discuss larger changes before submitting a PR.

git clone https://github.com/costajohnt/ink-autocomplete.git
cd ink-autocomplete
npm install
npm run build
npm test

The test suite uses vitest with ink-testing-library. CI runs against Node 20 and 22.

Changelog

See GitHub Releases.

License

MIT

Keywords

ink

FAQs

Package last updated on 25 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts