Aria Autocomplete On GitHub Accessible, extensible, plain JavaScript autocomplete with multi-select

Key features

  • Single and multiple selection support
  • Accessibility: Use of ARIA attributes, custom screen reader announcements, and tested with assistive technologies.
  • Progressive enhancement: Automatic source building through specifying a <select> as the element, or an element with child checkboxes.
  • Extensible source options: Array of Strings or Objects, Function (or Promise), or an endpoint.
  • Compatibility: Broad browser and device support (IE9+).
  • Starting values: Automatic selection based on starting values, including for checkboxes, select options, and for async handling.
  • Small script size: 10 kB gzipped.

Grab from NPM and use in a module system, or grab the code from unpkg:

npm install aria-autocomplete
<script src="https://unpkg.com/aria-autocomplete"></script>

And call with an element and options:

AriaAutocomplete(element, options);

Examples

1. Array as Source, using default options

When the source is an Array, it can either be an Array of Strings:

AriaAutocomplete(document.getElementById('input'), {
    source: [
        'Afghanistan',
        'Albania',
        'Algeria',
        ...etc
    ]
});

Or you can use an Array of Objects with value and / or label properties (if only a value or label is provided, that property will be used for both):

AriaAutocomplete(document.getElementById('input'), {
    source: [
        {
            value: 'AFG',
            label: 'Afghanistan'
        }, 
        {
            value: 'ALB',
            label: 'Albania'
        },
        ...etc
    ]
});

The autocomplete will search by the label property by default, and set the value in the original element (if an <input /> was used).

You can add an Array of Strings to the alsoSearchIn option to indicate other properties to filter by as well as the label.

If your objects don't have value and / or label properties, and you're unable to transform them ahead of use, there is also a sourceMapping option:

AriaAutocomplete(document.getElementById('input'), {
    source: [
        {
            code3: 'AFG',
            name:' Afghanistan'
        }, 
        {
            code3: 'ALB',
            name: 'Albania'
        },
        ...etc
    ],
    sourceMapping: { 'value': 'code3', 'label': 'name' },
    alsoSearchIn: ['value']
});

2. Progressive Enhancement, using Element(s) as Source

(With result creation, multi-select, autogrow, results limit, and show all control)

AriaAutocomplete(document.querySelector('select'), {
    placeholder: 'Type to refine',
    deleteOnBackspace: true,
    showAllControl: true,
    autoGrow: true,
    maxResults: 50,
    create: true,
    minLength: 0,
    maxItems: 5
});

If the source option is falsy, or is an empty Array, the autocomplete will check to see if the element used is a <select>, or if there are any checkboxes inside the element, and will build up a source Array from what's available.

This example uses a <select> element; so when an option in the autocomplete is selected (or removed), the corresponding <option> element will also be selected (or deselected) as well.

Any pre-selected <option> or checkbox elements are respected, and the multiple option will also be set to true automatically if the <select> has that attribute set. If checkboxes are used, the multiple option will always be set to true.

When using the create option along with progressive enhancement, the created selections will be added to the original <select> element or checkbox list. So they will always remain as available options (both internally, and in the DOM) even after being de-selected.

3. Function as Source

(With delete all control and multi-select, but without autogrow)

If you use a Function for the source with your own filtering logic, it will be passed the search query, and a render Function, which expects an Array (using either of the formats mentioned above).

AriaAutocomplete(document.getElementById('input'), {
    maxItems: 4,
    maxResults: 20,
    multiple: true,
    deleteAllControl: true,
    placeholder: 'Type to search',
    source: (query: string, render: (toRender: any[]) => void, isFirstCall: boolean) => {
        const toRender: any[] = [];
        // build up your Array here, then render...
        render(toRender);
    },
    onItemRender: (itemData: any) => {
        return `${itemData.label} (${itemData.value})`;
    }
});

The source function can also be a Promise which resolves with the items to render, instead of having to use the provided second argument callback.

If the autocomplete is initialised on an <input /> with a starting value, the source function will also be called immediately using that value. In this case, the render function expects an Array of possible entries to check that starting value against, to determine what the starting label (or multi-select elements) should be.

The onItemRender callback is also being used here to display the code next to the country name in the results.

The initialised input's starting value is "GLP,ZWE" - the country codes for Guadeloupe and Zimbabwe respectively.

4. String (endpoint) as Source

If you want to send the query to an endpoint, you can do that too.

AriaAutocomplete(document.getElementById('input'), {
    source: 'some-url-here/endpoint',
    onAsyncPrep: (url: string) => url,
    onAsyncSuccess: (query: string, xhr: XMLHttpRequest, isFirstCall: boolean) => {
        return JSON.parse(xhr.responseText);
    },
    asyncMaxResultsParam: 'limit',
    asyncQueryParam: 'q',
    maxResults: 25
});

As with a Function as the source, if the autocomplete is initialised on an input with a starting value, the endpoint will be called immediately.

The current query and maxResults will be added to the url using the asyncQueryParam and asyncMaxResultsParam options respectively.

E.g. https://some-url-here/endpoint?q=norway&limit=20.

You can also use the onAsyncPrep callback to modify the URL.

This example has no filtering in the request, as it's just requesting a JSON file, so it's doing some basic filtering in the onAsyncSuccess callback. The initialised input's starting value is "CAN" (the code3 property for Canada).

5. Array as Source, with Result Creation and Multi-Select

This example simulates a tagging input. It has a small source array, but with result creation enabled, and includes starting values not included in the source.

const autocomplete = AriaAutocomplete(document.getElementById('input'), {
    source: ['Sophie', 'John', ...more],
    onItemRender: ({ label }) => {
        if (!autocomplete.options.source.includes(label)) {
            return `Add ${label}...`;
        }
        return label;
    },
    deleteOnBackspace: true,
    autoGrow: true,
    multiple: true,
    minLength: 0,
    create: true
});

All component options that accept a Function will have their context (this) set to include the full autocomplete API (assuming you use an ordinary function expression instead of arrow functions). The only exception to this is the Function and Endpoint starting cases mentioned above, where the context is not manually set. So you can check for the API's existence, or check the context against the window object, to check if it's the starting call (or use the provided final param in the relevant callbacks).

Options

The full list of available options is as follows:

Core options

name: string
Give the autocomplete a name so that (in single-select mode) its value will be included in form submissions.
(Instead of using this option, I would advise initialising the autocomplete on an existing input that will be submitted; this approach is compatible with the control in multiple mode)
source: string[] | any[] | string | Function | Promise
string for async endpoint, array of strings, array of objects with value and label, or function
sourceMapping: Object
an object detailing the properties to use for the label and value when using an Array of Objects as the source - see Array examples above.
alsoSearchIn: string[]
Additional properties to use when searching for a match
create: boolean | Function
If no exact match is found, create an entry in the results list for the current search text. Can use a function that receives the search term, and returns a string or an object (like a normal static source entry).
Default: false
delay: number
input delay before running a search
Default: 100
minLength: number
Minimum number of characters to run a search (includes spaces)
Default: 1
maxResults: number
Maximum number of results to render
Default: 9999
showAllControl: boolean
Render a button that triggers showing all options
Default: false
confirmOnBlur: boolean | Function
Confirm current selection (from using arrow keys) when blurring off of the control. Will also check for a label match if there is no current selection.
Can use a function which receives the search term and results, and returns a string to be used to compare against the result labels.
Default: true
multiple: boolean
Allow multiple items to be selected
Default: false
autoGrow: boolean
Set input width to match its content
Default: false
maxItems: number
Maximum number of items that can be selected in multiple mode
Default: 9999
multipleSeparator: string
In multiple mode, if the original element was an input, the character that separates the values
Default: `,`
deleteOnBackspace: boolean
If input is empty and in multiple mode, delete last selected item on backspace
Default: false
deleteAllControl: boolean
In multiple mode, if more than 1 item is selected, add a button at the beginning of the selected items as a shortcut to delete all
Default: false
deleteAllText: string
Text to use in the deleteAllControl
Default: `Delete all`

Async mode options

asyncQueryParam: string
When source is a string, param to use when adding input value to it
Default: `q`
asyncMaxResultsParam: string
When source is a string, param to use when adding maxResults option (+ current selected count in multiple mode) to it
Default: `limit`

Styling / rendering options

placeholder: string
Placeholder text to show in generated input
Default: ``
noResultsText: string
Text to show (and announce to screen readers) if no results are found. If this is an empty string, the list will close instead
Default: `No results`
cssNameSpace: string
The string to prepend to all main classes for BEM naming e.g. `aria-autocomplete__input`
(Some other generic class-names are used alongside these as well, such as `disabled`, `focused`, `hidden`, etc.)
Default: `aria-autocomplete`
listClassName: string
Custom class name to add to the generated list element
Default: ``
inputClassName: string
Custom class name to add to the generated input element
Default: ``
wrapperClassName: string
Custom class name to add to the generated component wrapper
Default: ``

Screen reader enhancements

srDelay: number
Set the delay in milliseconds before screen reader announcements are made.
Note: if this is too short, some default announcements may interrupt it.
Default: 5000
srAutoClear: boolean | number
Automatically clear the screen reader announcement element after the specified delay. Number is in milliseconds.
Default: 10000
srDeleteText: string
In multi mode, screen reader text used for element deletion - prepended to label
Default: `delete`
srDeletedText: string
Screen reader text announced after deletion - appended to label
Default: `deleted`
srShowAllText: string
Screen reader text for the show all button
Default: `Show all`
srSelectedText: string
Screen reader text announced after selection - appended to label
Default: `selected`
srListLabelText: string
Screen reader explainer added to the list element via aria-label attribute
Default: `Search suggestions`
srAssistiveText: string
Screen reader description used for main input when empty
Default: `When results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.`
srAssistiveTextAutoClear: boolean
Automatically remove the srAssistiveText once user input is detected, to reduce screen reader verbosity.
The text is re-associated with the generated input if its value is emptied.
Default: 5000
srResultsText: Function
Screen reader announcement after results are rendered
Default: length => `${length} ${length === 1 ? 'result' : 'results'} available.`

Callbacks

onSearch: Function
Before search is performed - can be used to affect search value by returning a new one
Default: (query: string) => query
onAsyncPrep: Function
Callback before async call is made - receives the URL. Can be used to format the endpoint URL by returning a String, and for changes to the XHR object.
Note: this is before the onload and onerror functions are attached, and before the open method is called Default: (url: string, xhr: XMLHttpRequest, isFirstCall: boolean) => url
onAsyncBeforeSend: Function
Callback before async call is sent - receives the XHR object. Can be used for final changes to the XHR object, such as adding auth headers
Default: (query: string, xhr: XMLHttpRequest, isFirstCall: boolean) => {}
onAsyncSuccess: Function
After async call succeeds, but before the results render - is passed the value and the XHR Object. Can be used to format the results by returning an Array
Default: (query: string, xhr: XMLHttpRequest, isFirstCall: boolean) => xhr.responseText
onAsyncComplete: Function
After async call completes successfully, and after the results have rendered.
Default: (query: string, xhr: XMLHttpRequest, isFirstCall: boolean) => {}
onAsyncError: Function
If async call fails - is passed the value and the XHR Object.
Default: (query, xhr: XMLHttpRequest, isFirstCall: boolean) => {}
onItemRender: Function
Called for each item rendered in the list - Can be used to format the <li> content by returning a String
Default: (itemData: any) => itemData.label
onResponse: Function
Prior to rendering - can be used to format the results by returning an Array
Default: (optionsToRender: any[]) => optionsToRender
onConfirm: Function
After an autocomplete selection is made
Default: (selection: any) => {}
onDelete: Function
After an autocomplete selection is deleted (programmatically in single-select mode, or by user action in multi-select mode)
Default: (deletion: any) => {}
onChange: Function
When the selected item(s) changes
Default: (selectedItems: any[]) => {}
onFocus: Function
When the overall component receives focus
Default: (componentWrapper: HTMLDivElement) => {}
onBlur: Function
When the overall component loses focus
Default: (componentWrapper: HTMLDivElement) => {}
onReady: Function
When main script processing and initial rendering has finished
Default: (componentWrapper: HTMLDivElement) => {}
onClose: Function
When list area closes
Default: (listElement: HTMLUListElement) => {}
onOpen: Function
When list area opens
Default: (listElement: HTMLUListElement) => {}