Skip to content

Autocomplete

Autocomplete provides fast filtering of large datasets through live search results based on user input.
Figma logo
A points to TextField; B points to a Search icon; C points to a text value; D points to a Spinner; E points to a clear button
Illustrating the pieces that make up an Autocomplete Field
Explaining the pieces that make up an Autocomplete Field
ItemNameDescription
A<TextField /><AutocompleteField /> uses a <TextField /> under the hood.
B<Search />The <Search /> icon is used when implementing a search box using <AutocompleteField />.
CValueThe placeholder or typed text string.
D<Spinner />Rendered when loading is true to give a visual cue that content is loading.
E<IconButton />The <Close /> icon is used within <IconButton /> to clear the entered or selected text string.
Showing the dimensions of an autocomplete field: 44px in height with width determined by its container
Showing a dotted line that outlines a clickable region
Illustrating the spacing within an Autocomplete Field
A points to the Autocomplete Listbox container; B points to an Autocomplete Option; C points to a group label; D points to an Autocomplete Option Group
Illustrating the pieces that make up an Autocomplete Listbox
Explaining the pieces that make up an Autocomplete Listbox
ItemNameDescription
AContainerThe parent component–rendered as a <ul>—that wraps <AutocompleteOption /> children components.
B<AutocompleteOption />The child component—rendered as an <li>—that contains the main content for a given step.
CGroup labelA <span> generated by the label prop when grouping options.
D<AutocompleteOptionGroup />A group of <AutocompleteOptions /> using a required label.
Autocomplete Listbox has a maximum height of 400px and its width is automatically determined by its container
Illustrating the dimensions of an Autocomplete Listbox
A points to the Autocomplete Listbox container; B points to an Autocomplete Option; C points to a group label; D points to an Autocomplete Option Group
Illustrating the pieces that make up an Autocomplete Option
Explaining the pieces that make up an Autocomplete Listbox
ItemNameDescription
A<Check />The <Check /> icon is used to indicate that the <AutocompleteOption /> is selected. The icon is omitted when using the search behavior.
BOption contentContains any other children passed to the <AutocompleteOption /> such as a text string or image.
The height and width of an Autocomplete Option is determined by its container
Showing a dotted line that outlines a clickable region
Illustrating the dimensions of an Autocomplete Option
  • Autocomplete supports two modes
  • The presence of <AutocompleteSelect /> is used to swap modes

This example implements filtering functionality in which options are limited to a specific dataset.

import React, { useMemo, useState } from 'react';
import {
AutocompleteField,
AutocompleteListbox,
AutocompleteOption,
AutocompleteSelect,
Option,
SubmitButton,
} from '@gusto/workbench';
import { FormLayout } from '@gusto/workbench-layouts';
import { Employee, countries, employees } from './_testData';
export const Basic = () => {
const [value, setValue] = useState('');
const [inputValue, setInputValue] = useState('');
const options = useMemo(() => {
const searchValue = inputValue.toLowerCase();
return countries
.filter(country => country.name.toLowerCase().startsWith(searchValue))
.slice(0, 6);
}, [inputValue]);
const selectedCountry = useMemo(
() => countries.find(country => country.code === value),
[value],
);
return (
<FormLayout
onSubmit={e => {
alert(new FormData(e.target as HTMLFormElement).get('basic'));
e.preventDefault();
}}
fields={
<AutocompleteField
label="Countries"
value={inputValue}
placeholder="Choose an option"
onChange={e => setInputValue(e.target.value)}
>
<AutocompleteSelect
name="basic"
value={value}
onChange={e => setValue(e.target.value)}
>
{selectedCountry ? (
<Option
label={selectedCountry.name}
value={selectedCountry.code}
/>
) : null}
</AutocompleteSelect>
<AutocompleteListbox>
{options.map(({ name, code }) => (
<AutocompleteOption key={code} value={code} label={name}>
{name}
</AutocompleteOption>
))}
</AutocompleteListbox>
</AutocompleteField>
}
actions={<SubmitButton>Submit</SubmitButton>}
/>
);
};

This example implements search functionality in which options are presented and selectable, but any input is considered valid.

import React, { useMemo, useState } from 'react';
import {
AutocompleteField,
AutocompleteListbox,
AutocompleteOption,
SubmitButton,
} from '@gusto/workbench';
import { FormLayout } from '@gusto/workbench-layouts';
import { Employee, countries, employees } from './_testData';
export const Search = () => {
const [inputValue, setInputValue] = useState('');
const options = useMemo(() => {
const searchValue = inputValue.toLowerCase();
return countries
.filter(country => country.name.toLowerCase().startsWith(searchValue))
.slice(0, 6);
}, [inputValue]);
return (
<FormLayout
onSubmit={e => {
alert(new FormData(e.target as HTMLFormElement).get('basic'));
e.preventDefault();
}}
fields={
<AutocompleteField
label="Countries"
value={inputValue}
placeholder="Choose an option"
onChange={e => setInputValue(e.target.value)}
name="basic"
>
<AutocompleteListbox>
{options.map(({ name, code }) => (
<AutocompleteOption key={code} label={name}>
{name}
</AutocompleteOption>
))}
</AutocompleteListbox>
</AutocompleteField>
}
actions={<SubmitButton>Submit</SubmitButton>}
/>
);
};

This example illustrates an asynchronous Autocomplete filter.

Search by name or title

import React, { useCallback, useState } from 'react';
import {
AutocompleteField,
AutocompleteListbox,
AutocompleteOption,
AutocompleteSelect,
Flex,
FlexItem,
Heading,
HeadingSubtext,
HeadingText,
Img,
Option,
SubmitButton,
useDebouncedCallback,
} from '@gusto/workbench';
import { Search as SearchIcon } from '@gusto/workbench-icons';
import { FormLayout } from '@gusto/workbench-layouts';
import { Employee, employees } from './_testData';
const searchEmployees = async (search: string): Promise<Employee[]> => {
return new Promise(resolve => {
const normalizedSearch = search.toLocaleLowerCase();
const artificialDelay = Math.floor(Math.random() * 1000);
setTimeout(() => {
const results = employees.filter(e => {
return (
e['Name-FL'].toLocaleLowerCase().includes(normalizedSearch) ||
e.Title.toLocaleLowerCase().includes(normalizedSearch)
);
});
resolve(results);
}, artificialDelay);
});
};
export const Async = () => {
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(
null,
);
const [options, setOptions] = useState<Employee[]>([]);
const [loading, setLoading] = useState(false);
const dbOnchange = useDebouncedCallback(
useCallback((searchValue: string) => {
setLoading(true);
searchEmployees(searchValue)
.then(users => {
// Only show the first 6 results
setOptions(users.slice(0, 6));
})
.finally(() => setLoading(false));
}, []),
300,
);
return (
<FormLayout
onSubmit={e => {
alert(new FormData(e.target as HTMLFormElement).get('basic'));
e.preventDefault();
}}
fields={
<AutocompleteField
name="basic"
label="Assignee"
helperText="Search by name or title"
before={<SearchIcon />}
loading={loading}
placeholder="Choose an option"
onChange={e => dbOnchange(e.target.value)}
>
<AutocompleteSelect
name="basic"
value={selectedEmployee?.Email ?? ''}
onChange={e => {
setSelectedEmployee(
options.find(opt => opt.Email === e.target.value) ?? null,
);
}}
>
{selectedEmployee ? (
<Option
label={selectedEmployee.Email}
value={selectedEmployee.Email}
/>
) : null}
</AutocompleteSelect>
<AutocompleteListbox>
{options.map(({ Email, Title, Avatar, 'Name-FL': fullName }) => (
<AutocompleteOption key={Email} label={fullName} value={Email}>
<Flex columnGap={3} alignItems="center">
<FlexItem width={48} borderRadius={1000} overflow="hidden">
<Img
src={Avatar}
alt={`Avatar for ${fullName}`}
width={500}
height={500}
/>
</FlexItem>
<FlexItem>
{/* Done for styling only— this has no semantic meaning because it is inside of an option */}
<Heading role="presentation" level={5} marginBottom={0}>
<HeadingText>{fullName}</HeadingText>
<HeadingSubtext>{Title}</HeadingSubtext>
</Heading>
</FlexItem>
</Flex>
</AutocompleteOption>
))}
</AutocompleteListbox>
</AutocompleteField>
}
actions={<SubmitButton>Submit</SubmitButton>}
/>
);
};

This example illustrates the use of <AutocompleteOptionGroup /> to group related options. <AutocompleteSeparator /> is also available to distinguish between options.

import React, { useMemo, useState } from 'react';
import {
AutocompleteField,
AutocompleteListbox,
AutocompleteOption,
AutocompleteOptionGroup,
AutocompleteSeparator,
SubmitButton,
} from '@gusto/workbench';
import { FormLayout } from '@gusto/workbench-layouts';
import { Country, Employee, countries, employees } from './_testData';
export const Grouped = () => {
const [inputValue, setInputValue] = useState('');
const options = useMemo(() => {
const searchValue = inputValue.toLowerCase();
return countries
.filter(country => country.name.toLowerCase().startsWith(searchValue))
.slice(0, 6);
}, [inputValue]);
const groupedOptions = useMemo(() => {
const sortedOptions = [...options].sort((a, b) => {
return a.name.localeCompare(b.name, undefined, {
sensitivity: 'base',
});
});
const groups: Record<string, Country[]> = {};
sortedOptions.forEach(option => {
// Key "A" for "Algeria"
const groupKey = option.name.slice(0, 1).toLocaleUpperCase();
const groupValue = groups[groupKey] ?? [];
groupValue.push(option);
groups[groupKey] = groupValue;
});
return Object.entries(groups);
}, [options]);
return (
<FormLayout
onSubmit={e => {
alert(new FormData(e.target as HTMLFormElement).get('basic'));
e.preventDefault();
}}
fields={
<AutocompleteField
label="Countries"
value={inputValue}
placeholder="Choose an option"
onChange={e => setInputValue(e.target.value)}
>
<AutocompleteListbox>
{groupedOptions.map(([groupLabel, groupOptions], index) => {
return (
<React.Fragment key={groupLabel}>
{index > 0 ? <AutocompleteSeparator /> : null}
<AutocompleteOptionGroup label={groupLabel}>
{groupOptions.map(({ name, code }) => (
<AutocompleteOption key={code} value={code} label={name}>
{name}
</AutocompleteOption>
))}
</AutocompleteOptionGroup>
</React.Fragment>
);
})}
</AutocompleteListbox>
</AutocompleteField>
}
actions={<SubmitButton>Submit</SubmitButton>}
/>
);
};

A basic Autocomplete filter implementation using Formik and Yup to require selection.

import React, { useCallback, useRef, useState } from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { Actions } from '@gusto/workbench';
import {
AutocompleteField,
AutocompleteListbox,
AutocompleteOption,
AutocompleteSelect,
FormLayout,
Option,
SubmitButton,
} from '@gusto/workbench-formik';
import { Country, countries } from './_testData';
export const Basic = () => {
const getOptions = useCallback((searchValue: string) => {
return countries
.filter(country => {
return country.name
.toLocaleLowerCase()
.startsWith(searchValue.toLocaleLowerCase());
})
.slice(0, 5);
}, []);
const [options, setOptions] = useState(() => getOptions(''));
const selectedCountry = useRef<Country | null>(null);
return (
<Formik
initialValues={{ country: '' }}
validationSchema={Yup.object().shape({
country: Yup.string().required('Country is required'),
})}
onSubmit={values => alert(JSON.stringify(values))}
>
<FormLayout
fields={
<AutocompleteField
label="Country"
onChange={e => setOptions(getOptions(e.target.value))}
>
<AutocompleteSelect name="country">
{selectedCountry.current ? (
<Option
label={selectedCountry.current.name}
value={selectedCountry.current.code}
/>
) : null}
</AutocompleteSelect>
<AutocompleteListbox>
{options.map(opt => (
<AutocompleteOption
key={opt.code}
label={opt.name}
value={opt.code}
onClick={() => {
selectedCountry.current = opt;
}}
/>
))}
</AutocompleteListbox>
</AutocompleteField>
}
actions={
<Actions>
<SubmitButton>Submit</SubmitButton>
</Actions>
}
/>
</Formik>
);
};

A basic Autocomplete search implementation using Formik and Yup to require input.

import React, { useCallback, useState } from 'react';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { Actions } from '@gusto/workbench';
import {
AutocompleteField,
AutocompleteListbox,
AutocompleteOption,
FormLayout,
SubmitButton,
} from '@gusto/workbench-formik';
import { countries } from './_testData';
export const Search = () => {
const getOptions = useCallback((searchValue: string) => {
return countries
.filter(country => {
return country.name
.toLocaleLowerCase()
.startsWith(searchValue.toLocaleLowerCase());
})
.slice(0, 5);
}, []);
const defaultInputValue = '';
const [options, setOptions] = useState(() => getOptions(defaultInputValue));
return (
<Formik
initialValues={{ country: '' }}
validationSchema={Yup.object().shape({
country: Yup.string().required('Country is required'),
})}
onSubmit={values => alert(JSON.stringify(values))}
>
<FormLayout
fields={
<AutocompleteField
name="country"
label="Country"
onChange={e => setOptions(getOptions(e.target.value))}
>
<AutocompleteListbox>
{options.map(opt => (
<AutocompleteOption
key={opt.code}
label={opt.name}
value={opt.code}
/>
))}
</AutocompleteListbox>
</AutocompleteField>
}
actions={
<Actions>
<SubmitButton>Submit</SubmitButton>
</Actions>
}
/>
</Formik>
);
};
React props
NameTypeDefaultDescription
after  
ReactNode
-Content shown after the input value. By default this content is not interactive and will ignore pointer events. Any interactive elements must be styled with pointer-events: auto;.
before  
ReactNode
-Content shown before the input value. By default this content is not interactive and will ignore pointer events. Any interactive elements must be styled with pointer-events: auto;.
children  
ReactNode
-Content of the field; typically an AutocompleteSelect and AutocompleteListbox
component  
ReactNode
-Component override for the input element
counter  
ReactNode
-Indicator of a limit placed on a field, e.g. maximum character count or remaining hours; most commonly using CharacterCountExternal link
fit  
ReactNode
containerDetermines the point of reference for the width of the component. If set to content, the size will be set by the size of the input. If set to container, the size will expand to fit the containing element.
helperText  
ReactNode
-A brief description or hint for the input; can be used to link to an external resource.
invalid  
boolean
-If true, the field will be shown in an invalid state.
label  Required
string
-Label associated with the input.
loading  
string
-If true, the loading Spinner is displayed.
optionalText  
string
-Text used to indicate that a field is not required.
validationText  
ReactNode
-Validation message associated with the input.
React props
NameTypeDefaultDescription
children  
ReactNode
-Content of the component, typically a series of AutocompleteOption
React props
NameTypeDefaultDescription
children  
ReactNode
-Content of the component
label  Required
string
-The text entered into the input element when the option is selected
value  Required
stringnumber
-The value for the option.
React props
NameTypeDefaultDescription
label  Required
string
-The group label
React props
NameTypeDefaultDescription
children  
ReactNode
-Content of the component, typically an Option

The following testing snippet(s) offer suggestions for testing the component using React Testing LibraryExternal link with occasional help from JestExternal link.

const countries = [
{ code: 'af', name: 'Afghanistan' },
// ...continued
];
// Tested component where state is being managed
const TestedComponent = () => {
const [inputValue, setInputValue] = useState('');
const [selectedCountry, setSelectedCountry] = useState<Country | null>(null);
const options = useMemo(() => {
const searchValue = inputValue.toLowerCase();
return countries
.filter(country => country.name.startsWith('A'))
.filter(country => country.name.toLowerCase().startsWith(searchValue))
.slice(0, 6);
}, [inputValue]);
return (
<AutocompleteField
label="Country"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
>
<AutocompleteSelect
name="country"
value={selectedCountry.code}
onChange={e => {
const newCountry = countries.find(country => country.code === e.target.value) ?? null;
setSelectedCountry(newCountry);
}}
>
{selectedCountry ? (
<Option
label={selectedCountry.name}
value={selectedCountry.code} />
) : null}
</AutocompleteSelect>
<AutocompleteListbox aria-label="Country suggestions">
<AutocompleteOptionGroup label="A">
{options.map(option => (
<AutocompleteOption key={option.code} label={option.name} value={option.code}>
{option.name}
</AutocompleteOption>
))}
</AutocompleteOptionGroup>
</AutocompleteListbox>
</AutocompleteField>
};
render(<TestedComponent />);
const combobox = screen.getByRole('combobox', {
name: 'Country',
});
// The listbox, clear button, and the options elements will all be hidden
// on first render. To unhide them, search for something:
userEvent.type(combobox, 'a');
expect(combobox).toHaveDisplayValue('a');
expect(listbox).toBeVisible();
const clearButton = screen.getByRole('button', {
name: 'Clear',
});
const listbox = screen.getByRole('listbox', {
name: 'Country suggestions',
});
const group = within(listbox).getByRole('group', {
name: 'A',
});
const afghanistanOption = within(group).getByRole('option', {
name: 'Afghanistan',
});
// To select an option:
userEvent.click(afghanistanOption);
// To clear the selected option:
userEvent.click(clearButton);
// Note: we recommend that you do not directly assert the selected value.
// Instead, assert displayed values and things you can see. If the selected
// value changes something else in the UI, assert on that instead