Lightweight JavaScript Autocomplete Library


This lightweight JavaScript autocomplete library adds typeahead suggestions, async remote search, and multi-select chips to any text input in plain HTML and JavaScript. It is a zero-dependency combobox that supports local arrays, async fetch sources, free-text custom values, match highlighting, and keyboard navigation - without React, jQuery, or a heavy UI toolkit.

What is hcg-autocomplete?

hcg-autocomplete is a free JavaScript typeahead library for <input type="text"> fields. You pass a source option: either a string array for client-side filtering, or a function that loads matches from your own API. As the user types, a dropdown lists suggestions with highlighted matches, arrow-key navigation, and proper combobox ARIA roles (role="combobox", aria-activedescendant).

Unlike a searchable <select>, suggestions are not fixed in the HTML. You can swap the source array at runtime with setSource(), load results after a debounced fetch, allow values that are not in the list (allowCustom), or collect several choices as removable chips when multiple is enabled. The original input stays in the DOM for labels, validation, and form posts.

hcg-autocomplete examples: typeahead with match highlighting on country names, and multi-select chips for frameworks and languages
hcg-autocomplete examples: typeahead with match highlighting on country names, and multi-select chips for frameworks and languages.

Why use hcg-autocomplete?

Native <datalist> cannot style the dropdown, debounce API calls, show a loading state, or build a tag picker. Framework autocomplete widgets often add a large bundle for one search field on an otherwise static page. This library fits dynamic suggestion lists from local arrays or your own API, with chips, custom values, and no extra dependencies. See the comparison or the feature list.

Live Demo

Try the widgets below. Type to filter local cities, pick multiple skills as chips, or search frameworks with a simulated async source. For every option on one page, open the hcg-autocomplete demo page.

Comparison Table

How hcg-autocomplete compares with common alternatives.

Feature hcg-autocomplete hcg-select Native datalist Heavy UI libs
Target elementText inputNative selectText inputVaries
DependenciesNoneNoneNoneOften jQuery or a framework
Dynamic / async sourceYesNo (fixed options)LimitedYes
Multi-select chipsYesNoNoYes
Free-text custom valuesYes (allowCustom)NoYesVaries
Match highlightingYesYesNoYes
Keyboard navigationYesYesBrowser-dependentYes
Form submissionYesYes (native select)YesVaries
Approximate sizeOne small JS file plus CSSOne small JS file plus CSSn/aLarge
License / costFree, MITFree, MITn/aVaries

Choose hcg-autocomplete when you need typeahead on a text input with local or remote data, optional tags, and free-text entry. Use hcg-select when you already have a <select> with fixed options. Use a heavier library when you need virtualized lists, complex templating, or framework-specific components.

Features

Everything included in the library at a glance:

  • Zero dependencies - one small JavaScript file and a companion CSS file.
  • Script tag or bundler - works with a plain script tag, CommonJS, or AMD.
  • Local array source - filter strings or { value, label } objects as the user types.
  • Async remote source - pass a function that receives the query and a done callback.
  • Built-in debounce - configurable delay before async or local filtering runs.
  • Multi-select chips - removable tags with multiple: true and optional maxItems.
  • Free-text values - allowCustom lets users commit text not in the list.
  • Match highlighting - query text is highlighted in suggestion labels.
  • Keyboard navigation - arrow keys, Enter, Escape, and Backspace for chips.
  • Clearable - optional clear button on single-select inputs.
  • Form-friendly - single mode keeps the input name; multiple mode adds a hidden field.
  • Callbacks - onInput, onOpen, onClose, onSelect, onRemove, and onChange.
  • Static helpers - get(), destroy(), and destroyAll().
  • Multiple instances - independent comboboxes on the same page.
  • React-friendly - create in useEffect and call destroy() on unmount.

Installation

Direct download
HTML
<link rel="stylesheet" href="hcg-autocomplete.css">
<script src="hcg-autocomplete.js"></script>
npm

Install from npm:

Command line
npm install hcg-autocomplete

CommonJS (require):

JavaScript
const hcgAutocomplete = require("hcg-autocomplete");
// also load hcg-autocomplete.css however your bundler handles CSS

ESM (import):

JavaScript
import hcgAutocomplete from "hcg-autocomplete";
import "hcg-autocomplete/hcg-autocomplete.css";
CDN usage

Load from a CDN with one script and one stylesheet - no npm or bundler required. The global hcgAutocomplete function is available as soon as the script finishes loading.

jsDelivr:

HTML
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hcg-autocomplete@1/hcg-autocomplete.css">
<script src="https://cdn.jsdelivr.net/npm/hcg-autocomplete@1/hcg-autocomplete.js"></script>

unpkg:

HTML
<link rel="stylesheet" href="https://unpkg.com/hcg-autocomplete@1/hcg-autocomplete.css">
<script src="https://unpkg.com/hcg-autocomplete@1/hcg-autocomplete.js"></script>

Basic Usage

Install hcg-autocomplete, add a text input to your markup, then call hcgAutocomplete() with a source array.

Initialize the input

Add a text input, then call hcgAutocomplete() with options.

HTML
<label for="city">City</label>
<input id="city" type="text" placeholder="Search cities..." autocomplete="off">
JavaScript
const ac = hcgAutocomplete('#city', {
  source: ['London', 'Paris', 'Berlin', 'Amsterdam'],
  minChars: 1
});
Instance methods: getValue() and setValue()

Read or set the current value programmatically. In multiple mode getValue() returns an array of values. Pass true as the second argument to setValue() to update the field without firing onChange or native input/change events.

JavaScript
const value = ac.getValue();
ac.setValue('Paris');
ac.setValue('London', true); // silent: no change event

// Multiple mode:
ac.setValue(['HTML', 'CSS', 'JavaScript']);
Update source with setSource()

Replace the suggestion list at runtime. Call refresh() to re-query with the current input text.

JavaScript
ac.setSource(['Orange', 'Lemon', 'Lime']);
ac.refresh();

// Or via setOption:
ac.setOption('source', ['Strawberry', 'Blueberry', 'Raspberry']);

Async Remote Source

Pass a function as source to load suggestions from an API. The function receives the query string and a done callback. Debounce is built in via the debounce option. Only the latest request is applied when responses arrive out of order.

The example below uses the free DummyJSON products API so you can try it in the browser without your own backend.

HTML
<label for="search">Product</label>
<input id="search" type="text" placeholder="Search products..." autocomplete="off">
JavaScript
hcgAutocomplete('#search', {
  minChars: 2,
  debounce: 300,
  source: (query, done) => {
    fetch('https://dummyjson.com/products/search?q=' + encodeURIComponent(query))
      .then((response) => response.json())
      .then((data) => {
        const lowerQuery = query.toLowerCase();
        done(null, data.products
          .filter((item) => item.title.toLowerCase().includes(lowerQuery))
          .map((item) => ({
            value: item.id,
            label: item.title
          })));
      })
      .catch((error) => done(error));
  }
});

While the request is in flight the panel shows the loadingText message. On error the panel shows the noResultsText message.

Multi-select Chips

Set multiple: true to let users pick several items. Selected values appear as removable chips above the input. Press Backspace on an empty input to remove the last chip. Use maxItems to cap how many can be selected.

HTML
<label for="skills">Skills</label>
<input id="skills" type="text" placeholder="Add skills..." autocomplete="off">
JavaScript
hcgAutocomplete('#skills', {
  multiple: true,
  maxItems: 5,
  source: ['HTML', 'CSS', 'JavaScript', 'TypeScript', 'React', 'Vue'],
  onChange: (input, values) => {
    console.log('Selected:', values);
  },
  onRemove: (input, item) => {
    console.log('Removed:', item.label);
  }
});

In multiple mode a hidden input receives comma-separated values for form submission. The visible text input is used only for typing new queries.

HTML Forms and required Validation

hcg-autocomplete does not add or remove the native required attribute. The browser runs standard HTML5 validation on the visible text input. How that behaves depends on single-select vs multiple mode and whether you need a committed selection or only non-empty text.

Single-select with required

In single-select mode the original <input> keeps its name and any attributes such as required. The browser checks that input.value is not empty.

HTML
<form id="city-form">
  <label for="city">City</label>
  <input id="city" name="city" type="text" required autocomplete="off">
  <button type="submit">Submit</button>
</form>
JavaScript
const cityAc = hcgAutocomplete('#city', {
  source: ['London', 'Paris', 'Berlin']
});

After the user picks from the list, the input shows the label and required passes. If the field is cleared with clear() or the clear button, validation fails as expected.

Limitation: required checks text, not selection

Native required only tests whether the input has text. A user can type characters without choosing a suggestion and the form may still submit, while getValue() stays empty. To require a real selection, validate getValue() on submit and use setCustomValidity() on the input:

JavaScript
const cityAc = hcgAutocomplete('#city', {
  source: ['London', 'Paris', 'Berlin'],
  onChange: (input, value) => {
    if (value) input.setCustomValidity('');
  }
});

document.getElementById('city-form').addEventListener('submit', (event) => {
  if (!cityAc.getValue()) {
    event.preventDefault();
    cityAc.input.setCustomValidity('Please select a city from the list.');
    cityAc.input.reportValidity();
    return;
  }
  cityAc.input.setCustomValidity('');
});
Multiple mode and required

In multiple mode the name attribute moves to a hidden input for form submission. The visible search input is often empty after chips are added. Do not use required on the search field. Validate getValue().length on submit instead.

If a submit fails and you call setCustomValidity(), that message stays on the input until you clear it. The browser may block the next submit before your handler runs. Clear custom validity in onChange when chips are added or removed.

HTML
<form id="skills-form">
  <label for="skills">Skills</label>
  <input id="skills" name="skills" type="text" autocomplete="off">
  <button type="submit">Submit</button>
</form>
JavaScript
const skillsAc = hcgAutocomplete('#skills', {
  multiple: true,
  source: ['HTML', 'CSS', 'JavaScript'],
  onChange: (input, values) => {
    input.setCustomValidity(values.length ? '' : '');
  }
});

document.getElementById('skills-form').addEventListener('submit', (event) => {
  if (!skillsAc.getValue().length) {
    event.preventDefault();
    skillsAc.input.setCustomValidity('Please add at least one skill.');
    skillsAc.input.reportValidity();
    return;
  }
  skillsAc.input.setCustomValidity('');
});
Moderequired on inputRecommended check
Single-selectWorks for non-empty textUse getValue() if a list selection is required
MultipleDo not use on the search inputUse getValue().length on submit and clear setCustomValidity() in onChange

Multiple Instances

Create as many autocomplete inputs on a page as you need. Each call to hcgAutocomplete() returns a fully independent instance with its own source, value, options, and API.

HTML
<input id="city" type="text" placeholder="City" autocomplete="off">
<br>
<input id="country" type="text" placeholder="Country" autocomplete="off">
JavaScript
const cityAc = hcgAutocomplete('#city', {
  source: ['London', 'Paris', 'Berlin']
});
const countryAc = hcgAutocomplete('#country', {
  source: ['United Kingdom', 'France', 'Germany']
});

cityAc.setValue('London');
// countryAc.open();
  • Link hcg-autocomplete.css once and initialize as many inputs as you need.
  • Static helpers accept a selector: hcgAutocomplete.get('#city').
  • Call each instance's destroy() when removing it from the DOM.

Options Reference

Pass these as the second argument to hcgAutocomplete(input, options). Options can be changed at runtime with setOption(name, value).

OptionTypeDefaultDescription
sourcearray | function[]Local array of strings or objects, or async function (query, done).
minCharsnumber1Minimum characters before suggestions appear.
debouncenumber300Milliseconds to wait after typing before filtering or calling async source.
multiplebooleanfalseEnable multi-select with removable chips.
allowCustombooleanfalseAllow Enter to commit text not in the suggestion list.
clearablebooleantrueShow clear button on single-select inputs.
highlightbooleantrueHighlight matching query text in labels.
maxItemsnumber | nullnullMaximum selections in multiple mode. null means no limit.
valueKeystring'value'Property name for submitted value on object items.
labelKeystring'label'Property name for displayed label on object items.
noResultsTextstring'No results'Message when no suggestions match.
loadingTextstring'Loading...'Message while async source is loading.
commitOnBlurbooleanfalseCommit typed text on blur when allowCustom is true.
onInputfunction-Called on input. Receives (input, query, api).
onOpenfunction-Called when panel opens. Receives (input, api).
onClosefunction-Called when panel closes. Receives (input, api).
onSelectfunction-Called when an item is selected. Receives (input, item, api).
onRemovefunction-Called when a chip is removed. Receives (input, item, api).
onChangefunction-Called when value changes. Receives (input, value, api).

Options

Copy-paste examples for each option. See Options Reference above for types and default values.

source

Array of strings, objects, or an async function. Default: [].

JavaScript
hcgAutocomplete('#city', {
  source: ['London', 'Paris', 'Berlin']
});

hcgAutocomplete('#country', {
  source: [
    { value: 'uk', label: 'United Kingdom' },
    { value: 'fr', label: 'France' }
  ]
});

hcgAutocomplete('#search', {
  source: (query, done) => {
    fetch('/api/search?q=' + encodeURIComponent(query))
      .then((response) => response.json())
      .then((data) => done(null, data))
      .catch((error) => done(error));
  }
});
minChars

Minimum characters before suggestions appear. Default: 1.

JavaScript
hcgAutocomplete('#search', {
  source: ['Red', 'Green', 'Blue'],
  minChars: 2
});
debounce

Milliseconds to wait after typing before filtering or calling an async source. Default: 300.

JavaScript
hcgAutocomplete('#search', {
  debounce: 400,
  source: (query, done) => {
    fetch('/api?q=' + encodeURIComponent(query))
      .then((r) => r.json())
      .then((data) => done(null, data));
  }
});
multiple

Enable multi-select with removable chips. Default: false.

JavaScript
hcgAutocomplete('#tags', {
  multiple: true,
  source: ['HTML', 'CSS', 'JavaScript']
});
allowCustom

Let users press Enter to commit text not in the suggestion list. Default: false.

JavaScript
hcgAutocomplete('#email-domain', {
  allowCustom: true,
  source: ['gmail.com', 'outlook.com', 'yahoo.com']
});
clearable

Show a clear button when a value is selected (single-select only). Default: true.

JavaScript
hcgAutocomplete('#city', {
  clearable: true,
  source: ['London', 'Paris']
});
highlight

Highlight matching query text in suggestion labels. Default: true.

JavaScript
hcgAutocomplete('#city', {
  highlight: true,
  source: ['Amsterdam', 'London', 'Paris']
});
maxItems

Maximum selections in multiple mode. Default: null (no limit).

JavaScript
hcgAutocomplete('#skills', {
  multiple: true,
  maxItems: 3,
  source: ['HTML', 'CSS', 'JavaScript', 'TypeScript']
});
valueKey

Property name for the submitted value when source items are objects. Default: 'value'.

JavaScript
hcgAutocomplete('#country', {
  valueKey: 'code',
  labelKey: 'name',
  source: [
    { code: 'uk', name: 'United Kingdom' },
    { code: 'fr', name: 'France' }
  ]
});
labelKey

Property name for the displayed label when source items are objects. Default: 'label'.

JavaScript
hcgAutocomplete('#product', {
  valueKey: 'sku',
  labelKey: 'title',
  source: [
    { sku: 'A100', title: 'Widget Pro' },
    { sku: 'B200', title: 'Gadget Lite' }
  ]
});
noResultsText

Message shown when no suggestions match. Default: 'No results'.

JavaScript
hcgAutocomplete('#search', {
  noResultsText: 'Nothing found',
  source: ['Apple', 'Banana']
});
loadingText

Message shown while an async source is loading. Default: 'Loading...'.

JavaScript
hcgAutocomplete('#search', {
  loadingText: 'Searching...',
  source: (query, done) => {
    fetch('/api?q=' + encodeURIComponent(query))
      .then((r) => r.json())
      .then((data) => done(null, data));
  }
});
commitOnBlur

Commit the typed text as a value when the input loses focus (requires allowCustom). Default: false.

JavaScript
hcgAutocomplete('#tag', {
  allowCustom: true,
  commitOnBlur: true,
  source: ['urgent', 'review', 'draft']
});

API Reference

hcgAutocomplete(input, options) returns an instance with these methods. The factory also exposes hcgAutocomplete.version, get(), destroy(), and destroyAll().

Method / PropertyReturnsDescription
open()instanceOpen the suggestion panel.
close()instanceClose the suggestion panel.
clear()instanceClear the current value and input text.
getValue()string | arrayCurrent value. Array in multiple mode.
setValue(value, silent?)instanceSet value programmatically. Pass true as the second argument to skip onChange and native events.
setSource(source)instanceReplace the suggestion source.
getSource()array | functionCurrent source option.
refresh()instanceRe-run source query with current input text.
setOption(name, value)instanceUpdate any option at runtime.
enable()instanceEnable the input.
disable()instanceDisable the input and close the panel.
destroy()instanceRemove listeners, unwrap DOM, and delete the instance.
elementElementThe wrapper .hcg-autocomplete element.
inputElementThe original text input element.
hcgAutocomplete.get(target)instance | nullStatic helper to retrieve an instance by element or selector.
hcgAutocomplete.destroy(target)booleanStatic helper to destroy an instance.
hcgAutocomplete.destroyAll()voidDestroy every instance on the page.
hcgAutocomplete.versionstringLibrary version string (currently '1.0.0').
open()

Open the suggestion panel and run the source query.

JavaScript
ac.open();
close()

Close the suggestion panel.

JavaScript
ac.close();
clear()

Clear the selected value and input text.

JavaScript
ac.clear();
getValue()

Return the current value. In multiple mode returns an array of value strings. You can call it on a stored instance or via hcgAutocomplete.get().

JavaScript
const value = ac.getValue();

const cityValue = hcgAutocomplete.get('#city').getValue();
setValue(value, silent?)

Set the value programmatically. Pass a string for single mode or an array for multiple mode. Pass true as the second argument to update the field without firing onChange or native input/change events.

JavaScript
ac.setValue('Paris');
ac.setValue('London', true); // silent: no change event
ac.setValue(['HTML', 'CSS']);
setSource(source) / getSource()

Replace or read the current suggestion source.

JavaScript
ac.setSource(['Orange', 'Lemon']);
const current = ac.getSource();
refresh()

Re-run the source query with the current input text.

JavaScript
ac.refresh();
setOption(name, value)

Update any option at runtime.

JavaScript
ac.setOption('minChars', 2);
ac.setOption('source', ['Red', 'Green', 'Blue']);
ac.setOption('onChange', (input, value) => console.log(value));
enable() / disable()

Enable or disable the input at runtime.

JavaScript
ac.disable();
ac.enable();
destroy()

Remove listeners, unwrap the DOM, and delete the instance reference.

JavaScript
ac.destroy();

// Without storing the instance:
hcgAutocomplete.destroy('#city');
Static: get(), destroy(), destroyAll(), version

Factory-level helpers and version string.

JavaScript
const ac = hcgAutocomplete.get('#city');
hcgAutocomplete.destroy('#city');
hcgAutocomplete.destroyAll();
console.log(hcgAutocomplete.version); // "1.0.0"

Callbacks

The library reports input, panel, selection, and value changes through callback options. Pass them when you create the instance, or update them later with setOption().

onInput

Fires on every input event after debounce scheduling. Receives (input, query, api).

JavaScript
hcgAutocomplete('#search', {
  source: ['London', 'Paris'],
  onInput: (input, query, api) => {
    console.log('Query:', query);
  }
});
onOpen

Fires when the suggestion panel opens. Receives (input, api).

JavaScript
hcgAutocomplete('#search', {
  source: ['London', 'Paris'],
  onOpen: (input, api) => {
    console.log('Panel opened');
  }
});
onClose

Fires when the suggestion panel closes. Receives (input, api).

JavaScript
hcgAutocomplete('#search', {
  source: ['London', 'Paris'],
  onClose: (input, api) => {
    console.log('Panel closed');
  }
});
onSelect

Fires when the user selects a suggestion. Receives (input, item, api) where item has value, label, and raw properties.

JavaScript
hcgAutocomplete('#search', {
  source: ['London', 'Paris'],
  onSelect: (input, item, api) => {
    console.log('Selected:', item.label, item.value);
  }
});
onRemove

Fires when a chip is removed in multiple mode. Receives (input, item, api).

JavaScript
hcgAutocomplete('#tags', {
  multiple: true,
  source: ['HTML', 'CSS', 'JavaScript'],
  onRemove: (input, item, api) => {
    console.log('Removed:', item.label);
  }
});
onChange

Fires when the committed value changes. Receives (input, value, api). In multiple mode value is an array of value strings.

JavaScript
hcgAutocomplete('#search', {
  source: ['London', 'Paris'],
  onChange: (input, value, api) => {
    console.log('Value changed:', value);
  }
});

Using with React

Import hcgAutocomplete from the package and create the combobox inside useEffect. Call destroy() in the cleanup function when the component unmounts.

Try the live React demo on StackBlitz

Autocomplete input component
JavaScript
import { useEffect, useRef } from "react";
import hcgAutocomplete from "hcg-autocomplete";
import "hcg-autocomplete/hcg-autocomplete.css";

function CitySearch({ onChange }) {
  const ref = useRef(null);

  useEffect(() => {
    const instance = hcgAutocomplete(ref.current, {
      source: ['London', 'Paris', 'Berlin'],
      onChange: (input, value) => onChange(value)
    });
    return () => instance.destroy();
  }, [onChange]);

  return (
    <input
      ref={ref}
      type="text"
      placeholder="Search cities..."
      autoComplete="off"
    />
  );
}

export default CitySearch;

Load hcg-autocomplete.css once in your app entry or import it in the component. Use disable() and enable() from props or effects when you need to lock the field during async work.

Styling and Customization

All default styles live in hcg-autocomplete.css, using CSS custom properties and plain class selectors. Edit that file directly, or override any rule from your own stylesheet loaded after it.

  • .hcg-autocomplete - root wrapper with CSS variables for colors, radius, and font size
  • .hcg-autocomplete-input - the text input inside the control
  • .hcg-autocomplete-panel - the dropdown suggestion panel
  • .hcg-autocomplete-list / .hcg-autocomplete-option - suggestion items
  • .hcg-autocomplete-chips / .hcg-autocomplete-chip - multi-select tags
  • .hcg-autocomplete-clear - the clear button on single-select inputs
  • .is-open / .is-multiple / .is-disabled - state classes on the wrapper
CSS
.hcg-autocomplete {
  --hcg-ac-border-focus: #7c3aed;
  --hcg-ac-hover: #f5f3ff;
  --hcg-ac-mark: #c4b5fd;
  --hcg-ac-radius: 10px;
}

.hcg-autocomplete-option.is-active {
  background: #ede9fe;
  color: #5b21b6;
}

Browser Support

hcg-autocomplete works in modern browsers that support ES5, DOM APIs, and ARIA attributes on combobox inputs.

BrowserSupported
Google ChromeYes
Mozilla FirefoxYes
Microsoft EdgeYes
Safari / iOS SafariYes
Mobile browsersYes

License

Released under the MIT License. Free for personal and commercial use. Copyright HTML Code Generator.

Frequently asked questions

What is the difference between hcg-autocomplete and hcg-select?

hcg-select enhances a native select element with search and keyboard navigation over fixed options. hcg-autocomplete enhances a text input with dynamic suggestions from a local array or async function, optional multi-select chips, and free-text custom values.

How do I load suggestions from an API?

Pass a function as the source option. The function receives the query string and a done callback. Call done(null, items) with the result array, or done(error) on failure. Debounce is built in via the debounce option.

Can users enter values not in the suggestion list?

Yes. Set allowCustom to true. Users can press Enter to commit the typed text as a value even when it does not match any suggestion.

Does hcg-autocomplete work in HTML forms?

Yes. In single-select mode the native input keeps its name and submits the selected label. In multiple mode a hidden input receives comma-separated values for form submission.

How does the required attribute work with hcg-autocomplete?

The library leaves the native required attribute on your input. In single-select mode the browser checks that the visible input is not empty. That does not guarantee the user picked from the list; use getValue() with setCustomValidity() on submit when a real selection is required. In multiple mode required on the search input is unreliable because the field is often empty after chips are added; validate getValue().length instead.

How do I enable multi-select chips?

Set multiple to true in the options object when you call hcgAutocomplete(). Selected items appear as removable chips. Use maxItems to limit how many can be selected.

Does hcg-autocomplete have any dependencies?

No. hcg-autocomplete is plain vanilla JavaScript with zero dependencies. Include hcg-autocomplete.js and hcg-autocomplete.css and you are ready to go.