@get-set/gs-autocomplete
v1.0.2
Published
Get-Set Autocomplete / Combobox
Downloads
153
Maintainers
Readme
@get-set/gs-autocomplete
A beautiful, accessible autocomplete / combobox that enhances a plain text
<input>. One codebase, four ways to use it:
window.GSAutocomplete(selectorOrElement, params)— vanilla factory$(selector).GSAutocomplete(params)— jQuery pluginelement.GSAutocomplete(params)—HTMLElement.prototypemethod<GSAutocomplete … />— React component with a typed imperativeref
The native targets and the React component share the same actions, constants, helpers, types and CSS, so every option behaves identically across all modes.
Features
- Any source — a static
{ label, value, group? }[]array, or an asyncfn(query) => Promise<Item[]>for remote search. - Filtering —
startsWith,contains,fuzzy(subsequence) or a fully custom predicate. Case-sensitive opt-in. - Multi-select — removable chips,
maxItems, dedupe, Backspace-to-remove. - Free text — commit arbitrary typed values not in the source.
- Grouping — bucket items under headings via their
groupfield. - Highlighting — bold the matched substring in each row.
- Custom rendering — return an HTML string per row (
itemRender), icons and secondary descriptions per item. - Async UX — loading state + spinner, debounce, stale-response guarding,
noResultsText. - Beautiful motion — 8 dropdown entrance animations, micro-interactions,
honoring
prefers-reduced-motion. - Full keyboard nav — Arrow Up/Down, Enter, Esc, Tab, Home, End, Backspace.
- ARIA combobox —
role="combobox"/listbox/option,aria-expanded,aria-activedescendant,aria-selected. - Theming —
light/dark/auto+ accent color, three sizes, RTL. - React-only
gsx— scoped per-instance styles injected on mount, removed on unmount. - Registry —
window.GSAutocompleteConfigue.instance(reference)to reach any live instance.
Compatibility
| Target | How |
| ----------------- | ------------------------------------------------------------- |
| Vanilla JS | window.GSAutocomplete(selectorOrElement, params) |
| jQuery | $(sel).GSAutocomplete(params) (auto-registered if jQuery present) |
| Prototype | el.GSAutocomplete(params) |
| React 16.8–19 | import GSAutocomplete from '@get-set/gs-autocomplete' |
React / ReactDOM are optional peer dependencies — the vanilla bundle has no React dependency.
Project layout
GSAutocomplete.ts native factory + window.GSAutocompleteConfigue registry
actions/ init, open/close, query, search, select, keynav, render, animate, refresh, destroy
helpers/ filter, layout, render, uihelpers
constants/ defaultParams, sizes, animations
types/ params, ref, global.d.ts
components/GSAutocomplete.tsx React component (forwardRef + useImperativeHandle)
components/styles/ GSAutocomplete.css/.scss + GSAutocompleteCSS.ts (string export)
styles/ GSAutocomplete.css/.scss (publishable)
example.html native showcase
tests/ vitest suite (logic + native + react)Install
npm i @get-set/gs-autocompleteBuild & test
npm install
npm run build # build:js (webpack -> dist-js/bundle.js) + build:react (tsc -> dist/)
npm test # vitest runnpm run build:jsbundles the native target todist-js/bundle.jsand appends a banner registering the jQuery ($.fn.GSAutocomplete) and prototype (HTMLElement.prototype.GSAutocomplete) adapters.npm run build:reactemits the React component + typings todist/.
Usage — Native JS
Include the bundle and the stylesheet, then call the factory with a CSS selector string or a DOM element.
<link rel="stylesheet" href="node_modules/@get-set/gs-autocomplete/styles/GSAutocomplete.css" />
<script src="node_modules/@get-set/gs-autocomplete/dist-js/bundle.js"></script>
<input id="fruit" placeholder="Search fruit…" />
<script>
const fruits = [
{ label: 'Apple', value: 'apple' },
{ label: 'Apricot', value: 'apricot' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry', group: 'Stone fruit' }
];
// (a) selector string
const ac = window.GSAutocomplete('#fruit', {
reference: 'fruit',
source: fruits,
filter: 'contains',
highlightMatch: true,
onChange: (value) => console.log('value:', value)
});
// (b) …or pass the element directly — this is exactly what the jQuery and
// prototype adapters do, so the factory MUST accept an element.
const el = document.querySelector('#fruit');
// window.GSAutocomplete(el, { source: fruits });
ac.setValue('banana');
console.log(ac.getValue()); // "banana"
</script>Instance methods (registry)
const ac = window.GSAutocompleteConfigue.instance('fruit');
ac.openDropdown();
ac.setValue(['apple', 'banana']); // multiple mode
ac.clear();
ac.destroy();Usage — jQuery
The bundle auto-registers $.fn.GSAutocomplete when window.jQuery is present.
Each matched element is enhanced (the adapter passes the element to the
factory).
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-autocomplete/dist-js/bundle.js"></script>
<input class="ac" />
<script>
$('.ac').GSAutocomplete({
source: ['Apple', 'Banana', 'Cherry'].map((l) => ({ label: l, value: l })),
placeholder: 'Type to search…'
});
// Reach the instance through the registry:
const ac = window.GSAutocompleteConfigue.instance('<reference>');
</script>Usage — Prototype
Every HTMLElement gains a .GSAutocomplete(params) method (also passes the
element to the factory):
<input id="city" />
<script>
document.getElementById('city').GSAutocomplete({
source: async (q) =>
(await fetch(`/api/cities?q=${encodeURIComponent(q)}`)).json(),
minChars: 2,
debounce: 250,
noResultsText: 'No cities found'
});
</script>Usage — React
import { useRef } from 'react';
import GSAutocomplete, {
GSAutocompleteHandle
} from '@get-set/gs-autocomplete';
const fruits = [
{ label: 'Apple', value: 'apple' },
{ label: 'Apricot', value: 'apricot' },
{ label: 'Banana', value: 'banana' }
];
export default function Demo() {
const ref = useRef<GSAutocompleteHandle>(null);
return (
<>
<GSAutocomplete
ref={ref}
source={fruits}
filter="fuzzy"
multiple
grouping
size="md"
theme="auto"
accentColor="#7c3aed"
placeholder="Pick fruit…"
onSelect={(item) => console.log('selected', item)}
onChange={(value) => console.log('value', value)}
gsx={{ '.gs-ac-option': { fontWeight: '500' } }}
/>
<button onClick={() => ref.current?.open()}>Open</button>
<button onClick={() => ref.current?.setValue(['apple', 'banana'])}>Set</button>
<button onClick={() => ref.current?.clear()}>Clear</button>
</>
);
}Controlled value:
const [value, setValue] = useState<string>('apple');
<GSAutocomplete source={fruits} value={value} onChange={(v) => setValue(v as string)} />gsx — scoped styles (React-only)
gsx accepts a nested CSS object. It is compiled and injected into <head>
scoped to this instance via [data-key='…'], and removed automatically on
unmount:
<GSAutocomplete
source={fruits}
gsx={{
'.gs-ac-control': { borderRadius: '999px' },
'.gs-ac-option.gs-ac-active': { background: 'rgba(124,58,237,.14)' }
}}
/>Options / props
Every option works in all four modes (native param key === React prop name).
Data
| Name | Type | Default | Description |
| --------------- | ------------------------------------------------------------------- | ------------ | ----------- |
| source | Item[] | (query) => Item[] \| Promise<Item[]> | undefined | Array source (filtered locally) or async query function (the authority for remote search). |
| filter | 'startsWith' | 'contains' | 'fuzzy' | (item, query) => boolean | 'contains' | Built-in strategy or custom predicate (array sources). |
| caseSensitive | boolean | false | Case-sensitive matching for the built-in filters. |
| minChars | number | 1 | Minimum characters before a search runs. |
| debounce | number | 200 | Debounce delay (ms) before searching. 0 = immediate. |
| maxResults | number | false | 50 | Cap rendered results. false = uncapped. |
| highlightMatch| boolean | true | Bold the matched substring in each row. |
| grouping | boolean | false | Group items by their group field under headings. |
| openOnFocus | boolean | false | Show suggestions immediately on focus. |
Selection behaviour
| Name | Type | Default | Description |
| --------------- | ------------------- | --------------------------- | ----------- |
| multiple | boolean | false | Multi-select with removable chips. |
| allowFreeText | boolean | false | Commit arbitrary typed text (Enter) not in the source. |
| closeOnSelect | boolean | true single / false multi | Close the dropdown after a selection. |
| clearOnSelect | boolean | false single / true multi | Clear the query text after selecting. |
| maxItems | number | false | false | Max chips allowed (multiple mode). |
Presentation
| Name | Type | Default | Description |
| --------------- | -------------------------- | -------------- | ----------- |
| placeholder | string | undefined | Input placeholder text. |
| clearable | boolean | true | Show the clear (×) button when there is a value. |
| disabled | boolean | false | Disable the whole control. |
| size | 'sm' | 'md' | 'lg' | 'md' | Control size preset. |
| noResultsText | string | 'No results' | Text shown when a search yields nothing. |
| loaderHtml | string | built-in | Custom spinner content (HTML string). |
| itemRender | (item, query, index) => string | undefined | Custom HTML for each option row. |
Animation
| Name | Type | Default | Description |
| ------------------- | ----------------------------- | -------------------------------- | ----------- |
| animation | GSAutocompleteAnimation | false | 'fade-up' | Dropdown entrance animation. false disables motion. |
| animationDuration | number | 180 | Duration in ms. |
| animationEasing | string | cubic-bezier(0.16,1,0.3,1) | CSS easing. |
| reducedMotion | 'auto' | boolean | 'auto' | Honor prefers-reduced-motion. |
Theming
| Name | Type | Default | Description |
| ------------- | ----------------------------------- | ------- | ----------- |
| theme | 'light' | 'dark' | 'auto' | 'auto'| Visual theme (auto follows prefers-color-scheme). |
| accentColor | string | #2563eb | Accent for active rows / focus ring (--gsa-accent). |
| rtl | boolean | false | Right-to-left layout. |
| customClass | GSAutocompleteCustomClass | undefined | Per-part class overrides. |
| className | string | undefined | Extra class on the wrapper root. |
Identity / value
| Name | Type | Default | Description |
| -------------- | ----------------------------------------------- | ------- | ----------- |
| reference | string | auto GUID | Registry key. |
| value | string | number | Array<string\|number> | undefined | Native: initial value. React: controlled value. |
| defaultValue | same | undefined | React only: uncontrolled initial value. |
| gsx | NestedCSS | undefined | React only: scoped per-instance styles. |
Callbacks
| Name | Type | Description |
| ---------- | ----------------------------- | ----------- |
| onSearch | (query: string) => void | Fired (debounced) when a search runs. |
| onSelect | (item: Item) => void | Fired when an item is selected / free text committed. |
| onChange | (value) => void | Fired when the committed value changes. |
| onOpen | () => void | Fired when the dropdown opens. |
| onClose | () => void | Fired when the dropdown closes. |
Item shape
interface GSAutocompleteItem {
label: string; // shown in the dropdown (and input in single mode)
value?: string | number; // committed value (defaults to label)
group?: string; // bucket heading when grouping
disabled?: boolean; // non-selectable
description?: string; // secondary line under the label
icon?: string; // leading icon (HTML string)
[key: string]: unknown; // passthrough data for callbacks / itemRender
}Variant catalogs
Filter catalog
| Value | Behaviour |
| ------------- | --------- |
| startsWith | Label starts with the query. |
| contains | Label contains the query (default). |
| fuzzy | Query characters appear in order (subsequence) in the label. |
| (fn) | Custom (item, query) => boolean predicate. |
Animation catalog
| Value | Motion |
| ------------- | ------ |
| fade | Opacity fade-in. |
| fade-up | Fade + rise from below (default). |
| fade-down | Fade + drop from above. |
| scale | Scale up from 96%. |
| slide-down | Slide down + clip. |
| slide-up | Slide up + clip. |
| flip | Perspective flip on the X axis. |
| expand | Expand from the top edge. |
| none | No motion (also the reduced-motion fallback). |
| false | Disables motion entirely. |
Size catalog
| Value | Control height / typography |
| ----- | --------------------------- |
| sm | Compact. |
| md | Default. |
| lg | Large. |
Theme catalog
| Value | Appearance |
| ------- | ---------- |
| light | Light surface. |
| dark | Dark surface. |
| auto | Follows prefers-color-scheme. |
Async / remote search
window.GSAutocomplete('#city', {
source: async (query) => {
const res = await fetch(`/api/cities?q=${encodeURIComponent(query)}`);
return res.json(); // [{ label, value }, …]
},
minChars: 2,
debounce: 250
});While a function source is in flight the dropdown shows the loading row / spinner. Stale responses are ignored via a monotonic request token, so only the latest query's results are rendered.
Imperative API
React GSAutocompleteHandle (via ref)
| Method | Signature | Description |
| ------------- | ----------------------------------------------------- | ----------- |
| open | () => void | Open the dropdown (runs a search with the current query). |
| close | () => void | Close the dropdown. |
| clear | () => void | Clear the selection / input value. |
| setValue | (value: string \| number \| Array<…>) => void | Set the committed value programmatically. |
| getValue | () => string \| number \| Array<…> | Get the committed value (string single / array multiple). |
| getSelected | () => Item \| Item[] \| null | Get the full selected item object(s). |
| focus | () => void | Focus the input. |
| refresh | () => void | Re-run the current search / re-render. |
| destroy | () => void | Unregister the instance from the registry. |
Native instance
window.GSAutocomplete(...) returns a ref with:
| Method | Description |
| --------------- | ----------- |
| openDropdown()| Open the dropdown (runs a search with the current input). |
| closeDropdown()| Close the dropdown. |
| isOpen() | Whether the dropdown is open. |
| clear() | Clear the selection / input value. |
| setValue(v) | Set the committed value. |
| getValue() | Get the committed value. |
| getSelected() | Get the full selected item object(s). |
| focus() | Focus the input. |
| refresh() | Rebuild the control from the current params. |
| setSource(s) | Replace the data source at runtime. |
| destroy() | Tear down and restore the original input. |
Registry — window.GSAutocompleteConfigue
window.GSAutocompleteConfigue.references; // Array<{ key, ref }>
window.GSAutocompleteConfigue.instance('myReference'); // -> ref | undefinedAccessibility
- Input:
role="combobox",aria-autocomplete="list",aria-haspopup="listbox",aria-expanded,aria-controls,aria-activedescendant. - List:
role="listbox"; options:role="option"+aria-selected+aria-disabled. - Full keyboard control: ↓/↑ move, Home/End jump, Enter select, Esc close, Tab select active + close, Backspace removes the last chip (multiple mode, empty input).
- All animations respect
prefers-reduced-motion.
License
ISC © Get-Set
