useSearchAutocomplete
Provides the behavior and accessibility implementation for a search autocomplete component. A search autocomplete combines a combobox with a searchfield, allowing users to filter a list of options to items matching a query.
install | yarn add @react-aria/autocomplete |
---|---|
version | 3.0.0-alpha.35 |
usage | import {useSearchAutocomplete} from '@react-aria/autocomplete' |
API#
useSearchAutocomplete<T>(
(props: AriaSearchAutocompleteOptions<T>,
, state: ComboBoxState<T>
)): SearchAutocompleteAria<T>
Features#
Autocomplete for search fields can be implemented using the <datalist> HTML element, but this has limited functionality and behaves differently across browsers.
useSearchAutocomplete
helps achieve accessible search field and autocomplete components that can be styled as needed.
- Support for filtering a list of options by typing
- Support for selecting a single option
- Support for disabled options
- Support for groups of items in sections
- Support for custom user input values
- Support for controlled and uncontrolled options, selection, input value, and open state
- Support for custom filter functions
- Async loading and infinite scrolling support
- Support for use with virtualized lists
- Exposed to assistive technology as a
combobox
with ARIA - Labeling support for accessibility
- Required and invalid states exposed to assistive technology via ARIA
- Support for mouse, touch, and keyboard interactions
- Keyboard support for opening the list box using the arrow keys, including automatically focusing the first or last item accordingly
- Support for opening the list box when typing, on focus, or manually
- Handles virtual clicks on the input from touch screen readers to toggle the list box
- Virtual focus management for list box option navigation
- Hides elements outside the input and list box from assistive technology while the list box is open in a portal
- Custom localized announcements for option focusing, filtering, and selection using an ARIA live region to work around VoiceOver bugs
Anatomy#
A search autocomplete consists of a label, an input which displays the current value, and a list box popup. Users can type within the input
to see search suggestions within the list box. The list box popup may be opened by a variety of input field interactions specified
by the menuTrigger
prop provided to useSearchAutocomplete
. useSearchAutocomplete
handles exposing
the correct ARIA attributes for accessibility for each of the elements comprising the search autocomplete. It should be combined
with useListBox, which handles the implementation of the popup list box.
useSearchAutocomplete
returns props that you should spread onto the appropriate elements:
Name | Type | Description |
labelProps | DOMAttributes | Props for the label element. |
inputProps | InputHTMLAttributes<HTMLInputElement> | Props for the search input element. |
listBoxProps | AriaListBoxOptions<T> | Props for the list box, to be passed to useListBox. |
clearButtonProps | AriaButtonProps | Props for the search input's clear button. |
descriptionProps | DOMAttributes | Props for the search autocomplete description element, if any. |
errorMessageProps | DOMAttributes | Props for the search autocomplete error message element, if any. |
isInvalid | boolean | Whether the input value is invalid. |
validationErrors | string[] | The current error messages for the input if it is invalid, otherwise an empty array. |
validationDetails | ValidityState | The native validation details for the input. |
State is managed by the useComboBoxState
hook from @react-stately/combobox
.
The state object should be passed as an option to useSearchAutocomplete
.
If the search field does not have a visible label, an aria-label
or aria-labelledby
prop must be provided instead to
identify it to assistive technology.
State management#
useSearchAutocomplete
requires knowledge of the options in order to handle keyboard
navigation and other interactions. It does this using the Collection
interface, which is a generic interface to access sequential unique keyed data. You can
implement this interface yourself, e.g. by using a prop to pass a list of item objects,
but useComboBoxState
from
@react-stately/combobox
implements a JSX based interface for building collections instead.
See Collection Components for more information,
and Collection Interface for internal details.
In addition, useComboBoxState
manages the state necessary for single selection and exposes
a SelectionManager
,
which makes use of the collection to provide an interface to update the selection state.
It also holds state to track if the popup is open, if the search field is focused, and the current input value.
For more information about selection, see Selection.
Example#
This example uses an <input>
element for the search field.
A "contains" filter function is obtained from useFilter
and is passed to useComboBoxState
so
that the list box can be filtered based on the option text and the current input text.
The list box popup should use the same Popover
and ListBox
components created with usePopover
and useListBox that you may already have in your component library or application. These can be shared with other
components such as a Select
created with useSelect or a Dialog
popover created with useDialog.
The code for these components is also included below in the collapsed sections.
In addition, see useListBox for examples of sections (option groups), and more complex options.
import {Item} from '@react-stately/collections';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useSearchAutocomplete} from '@react-aria/autocomplete';
import {useFilter} from '@react-aria/i18n';
// Reuse the ListBox and Popover from your component library. See below for details.
import {ListBox, Popover} from 'your-component-library';
function SearchAutocomplete(props) {
// Setup filter function and state.
let { contains } = useFilter({ sensitivity: 'base' });
let state = useComboBoxState({ ...props, defaultFilter: contains });
// Setup refs and get props for child elements.
let inputRef = React.useRef(null);
let listBoxRef = React.useRef(null);
let popoverRef = React.useRef(null);
let buttonRef = React.useRef(null);
let { inputProps, listBoxProps, labelProps, clearButtonProps } =
useSearchAutocomplete(
{
...props,
popoverRef,
listBoxRef,
inputRef
},
state
);
let { buttonProps } = useButton(clearButtonProps, buttonRef);
return (
<div style={{ display: 'inline-flex', flexDirection: 'column' }}>
<label {...labelProps}>{props.label}</label>
<div style={{ position: 'relative', display: 'inline-block' }}>
<input
{...inputProps}
ref={inputRef}
style={{
height: 24,
boxSizing: 'border-box',
marginRight: 0,
fontSize: 16
}}
/>
{state.inputValue !== '' &&
<button {...buttonProps} ref={buttonRef}>❎</button>}
{state.isOpen &&
(
<Popover
state={state}
triggerRef={inputRef}
popoverRef={popoverRef}
isNonModal
placement="bottom start"
>
<ListBox
{...listBoxProps}
listBoxRef={listBoxRef}
state={state}
/>
</Popover>
)}
</div>
</div>
);
}
<SearchAutocomplete label="Search Animals">
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
import {Item} from '@react-stately/collections';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useSearchAutocomplete} from '@react-aria/autocomplete';
import {useFilter} from '@react-aria/i18n';
// Reuse the ListBox and Popover from your component library. See below for details.
import {ListBox, Popover} from 'your-component-library';
function SearchAutocomplete(props) {
// Setup filter function and state.
let { contains } = useFilter({ sensitivity: 'base' });
let state = useComboBoxState({
...props,
defaultFilter: contains
});
// Setup refs and get props for child elements.
let inputRef = React.useRef(null);
let listBoxRef = React.useRef(null);
let popoverRef = React.useRef(null);
let buttonRef = React.useRef(null);
let {
inputProps,
listBoxProps,
labelProps,
clearButtonProps
} = useSearchAutocomplete(
{
...props,
popoverRef,
listBoxRef,
inputRef
},
state
);
let { buttonProps } = useButton(
clearButtonProps,
buttonRef
);
return (
<div
style={{
display: 'inline-flex',
flexDirection: 'column'
}}
>
<label {...labelProps}>{props.label}</label>
<div
style={{
position: 'relative',
display: 'inline-block'
}}
>
<input
{...inputProps}
ref={inputRef}
style={{
height: 24,
boxSizing: 'border-box',
marginRight: 0,
fontSize: 16
}}
/>
{state.inputValue !== '' &&
(
<button {...buttonProps} ref={buttonRef}>
❎
</button>
)}
{state.isOpen &&
(
<Popover
state={state}
triggerRef={inputRef}
popoverRef={popoverRef}
isNonModal
placement="bottom start"
>
<ListBox
{...listBoxProps}
listBoxRef={listBoxRef}
state={state}
/>
</Popover>
)}
</div>
</div>
);
}
<SearchAutocomplete label="Search Animals">
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
import {Item} from '@react-stately/collections';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useSearchAutocomplete} from '@react-aria/autocomplete';
import {useFilter} from '@react-aria/i18n';
// Reuse the ListBox and Popover from your component library. See below for details.
import {
ListBox,
Popover
} from 'your-component-library';
function SearchAutocomplete(
props
) {
// Setup filter function and state.
let { contains } =
useFilter({
sensitivity: 'base'
});
let state =
useComboBoxState({
...props,
defaultFilter:
contains
});
// Setup refs and get props for child elements.
let inputRef = React
.useRef(null);
let listBoxRef = React
.useRef(null);
let popoverRef = React
.useRef(null);
let buttonRef = React
.useRef(null);
let {
inputProps,
listBoxProps,
labelProps,
clearButtonProps
} =
useSearchAutocomplete(
{
...props,
popoverRef,
listBoxRef,
inputRef
},
state
);
let { buttonProps } =
useButton(
clearButtonProps,
buttonRef
);
return (
<div
style={{
display:
'inline-flex',
flexDirection:
'column'
}}
>
<label
{...labelProps}
>
{props.label}
</label>
<div
style={{
position:
'relative',
display:
'inline-block'
}}
>
<input
{...inputProps}
ref={inputRef}
style={{
height: 24,
boxSizing:
'border-box',
marginRight:
0,
fontSize: 16
}}
/>
{state
.inputValue !==
'' &&
(
<button
{...buttonProps}
ref={buttonRef}
>
❎
</button>
)}
{state.isOpen &&
(
<Popover
state={state}
triggerRef={inputRef}
popoverRef={popoverRef}
isNonModal
placement="bottom start"
>
<ListBox
{...listBoxProps}
listBoxRef={listBoxRef}
state={state}
/>
</Popover>
)}
</div>
</div>
);
}
<SearchAutocomplete label="Search Animals">
<Item key="red panda">
Red Panda
</Item>
<Item key="cat">
Cat
</Item>
<Item key="dog">
Dog
</Item>
<Item key="aardvark">
Aardvark
</Item>
<Item key="kangaroo">
Kangaroo
</Item>
<Item key="snake">
Snake
</Item>
</SearchAutocomplete>
Popover#
The Popover
component is used to contain the popup listbox for the SearchAutocomplete.
It can be shared between many other components, including Select,
Menu, and others.
See usePopover for more examples of popovers.
Show code
import type {AriaPopoverProps} from 'react-aria';
import type {OverlayTriggerState} from 'react-stately';
import {usePopover, Overlay, DismissButton} from '@react-aria/overlays';
interface PopoverProps extends AriaPopoverProps {
children: React.ReactNode,
state: OverlayTriggerState
}
function Popover({children, state, ...props}: PopoverProps) {
let {popoverProps} = usePopover(props, state);
return (
<Overlay>
<div
{...popoverProps}
ref={props.popoverRef as React.RefObject<HTMLDivElement>}
style={{
...popoverProps.style,
background: 'lightgray',
border: '1px solid gray'
}}>
{children}
<DismissButton onDismiss={state.close} />
</div>
</Overlay>
);
}
import type {AriaPopoverProps} from 'react-aria';
import type {OverlayTriggerState} from 'react-stately';
import {
DismissButton,
Overlay,
usePopover
} from '@react-aria/overlays';
interface PopoverProps extends AriaPopoverProps {
children: React.ReactNode;
state: OverlayTriggerState;
}
function Popover(
{ children, state, ...props }: PopoverProps
) {
let { popoverProps } = usePopover(props, state);
return (
<Overlay>
<div
{...popoverProps}
ref={props.popoverRef as React.RefObject<
HTMLDivElement
>}
style={{
...popoverProps.style,
background: 'lightgray',
border: '1px solid gray'
}}
>
{children}
<DismissButton onDismiss={state.close} />
</div>
</Overlay>
);
}
import type {AriaPopoverProps} from 'react-aria';
import type {OverlayTriggerState} from 'react-stately';
import {
DismissButton,
Overlay,
usePopover
} from '@react-aria/overlays';
interface PopoverProps
extends
AriaPopoverProps {
children:
React.ReactNode;
state:
OverlayTriggerState;
}
function Popover(
{
children,
state,
...props
}: PopoverProps
) {
let { popoverProps } =
usePopover(
props,
state
);
return (
<Overlay>
<div
{...popoverProps}
ref={props
.popoverRef as React.RefObject<
HTMLDivElement
>}
style={{
...popoverProps
.style,
background:
'lightgray',
border:
'1px solid gray'
}}
>
{children}
<DismissButton
onDismiss={state
.close}
/>
</div>
</Overlay>
);
}
ListBox#
The ListBox
and Option
components are used to show the filtered list of options as the
user types in the SearchAutocomplete. They can also be shared with other components like a Select. See
useListBox for more examples, including sections and more complex items.
Show code
import {useListBox, useOption} from '@react-aria/listbox';
function ListBox(props) {
let ref = React.useRef(null);
let { listBoxRef = ref, state } = props;
let { listBoxProps } = useListBox(props, state, listBoxRef);
return (
<ul
{...listBoxProps}
ref={listBoxRef}
style={{
margin: 0,
padding: 0,
listStyle: 'none',
maxHeight: 150,
overflow: 'auto',
minWidth: 200
}}
>
{[...state.collection].map((item) => (
<Option
key={item.key}
item={item}
state={state}
/>
))}
</ul>
);
}
function Option({ item, state }) {
let ref = React.useRef(null);
let { optionProps, isSelected, isFocused, isDisabled } = useOption(
{ key: item.key },
state,
ref
);
let backgroundColor;
let color = 'black';
if (isSelected) {
backgroundColor = 'blueviolet';
color = 'white';
} else if (isFocused) {
backgroundColor = 'gray';
} else if (isDisabled) {
backgroundColor = 'transparent';
color = 'gray';
}
return (
<li
{...optionProps}
ref={ref}
style={{
background: backgroundColor,
color: color,
padding: '2px 5px',
outline: 'none',
cursor: 'pointer'
}}
>
{item.rendered}
</li>
);
}
import {useListBox, useOption} from '@react-aria/listbox';
function ListBox(props) {
let ref = React.useRef(null);
let { listBoxRef = ref, state } = props;
let { listBoxProps } = useListBox(
props,
state,
listBoxRef
);
return (
<ul
{...listBoxProps}
ref={listBoxRef}
style={{
margin: 0,
padding: 0,
listStyle: 'none',
maxHeight: 150,
overflow: 'auto',
minWidth: 200
}}
>
{[...state.collection].map((item) => (
<Option
key={item.key}
item={item}
state={state}
/>
))}
</ul>
);
}
function Option({ item, state }) {
let ref = React.useRef(null);
let { optionProps, isSelected, isFocused, isDisabled } =
useOption({ key: item.key }, state, ref);
let backgroundColor;
let color = 'black';
if (isSelected) {
backgroundColor = 'blueviolet';
color = 'white';
} else if (isFocused) {
backgroundColor = 'gray';
} else if (isDisabled) {
backgroundColor = 'transparent';
color = 'gray';
}
return (
<li
{...optionProps}
ref={ref}
style={{
background: backgroundColor,
color: color,
padding: '2px 5px',
outline: 'none',
cursor: 'pointer'
}}
>
{item.rendered}
</li>
);
}
import {
useListBox,
useOption
} from '@react-aria/listbox';
function ListBox(props) {
let ref = React.useRef(
null
);
let {
listBoxRef = ref,
state
} = props;
let { listBoxProps } =
useListBox(
props,
state,
listBoxRef
);
return (
<ul
{...listBoxProps}
ref={listBoxRef}
style={{
margin: 0,
padding: 0,
listStyle:
'none',
maxHeight: 150,
overflow: 'auto',
minWidth: 200
}}
>
{[
...state
.collection
].map((item) => (
<Option
key={item.key}
item={item}
state={state}
/>
))}
</ul>
);
}
function Option(
{ item, state }
) {
let ref = React.useRef(
null
);
let {
optionProps,
isSelected,
isFocused,
isDisabled
} = useOption(
{ key: item.key },
state,
ref
);
let backgroundColor;
let color = 'black';
if (isSelected) {
backgroundColor =
'blueviolet';
color = 'white';
} else if (isFocused) {
backgroundColor =
'gray';
} else if (
isDisabled
) {
backgroundColor =
'transparent';
color = 'gray';
}
return (
<li
{...optionProps}
ref={ref}
style={{
background:
backgroundColor,
color: color,
padding:
'2px 5px',
outline: 'none',
cursor: 'pointer'
}}
>
{item.rendered}
</li>
);
}
Usage#
The following examples show how to use the SearchAutocomplete component created in the above example.
Uncontrolled#
The following example shows how you would create an uncontrolled SearchAutocomplete. The input value, selected option, and open state is completely uncontrolled.
<SearchAutocomplete label="Search Animals">
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete label="Search Animals">
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete label="Search Animals">
<Item key="red panda">
Red Panda
</Item>
<Item key="cat">
Cat
</Item>
<Item key="dog">
Dog
</Item>
<Item key="aardvark">
Aardvark
</Item>
<Item key="kangaroo">
Kangaroo
</Item>
<Item key="snake">
Snake
</Item>
</SearchAutocomplete>
Dynamic collections#
SearchAutocomplete follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time.
As seen below, an iterable list of options is passed to the SearchAutocomplete using the defaultItems
prop. The input's value is passed to the
onSubmit
handler, along with a key if the event was triggered by selecting an item from the listbox.
function Example() {
let options = [
{id: 1, name: 'Aerospace'},
{id: 2, name: 'Mechanical'},
{id: 3, name: 'Civil'},
{id: 4, name: 'Biomedical'},
{id: 5, name: 'Nuclear'},
{id: 6, name: 'Industrial'},
{id: 7, name: 'Chemical'},
{id: 8, name: 'Agricultural'},
{id: 9, name: 'Electrical'}
];
let [major, setMajor] = React.useState(null);
let onSubmit = (value, key) => {
if (value) {
setMajor(value);
} else if (key) {
setMajor(options.find(o => o.id === key).name);
}
};
return (
<>
<SearchAutocomplete
label="Search engineering majors"
defaultItems={options}
onSubmit={onSubmit}>
{(item) => <Item>{item.name}</Item>}
</SearchAutocomplete>
<p>Results for: {major}</p>
</>
);
}
function Example() {
let options = [
{id: 1, name: 'Aerospace'},
{id: 2, name: 'Mechanical'},
{id: 3, name: 'Civil'},
{id: 4, name: 'Biomedical'},
{id: 5, name: 'Nuclear'},
{id: 6, name: 'Industrial'},
{id: 7, name: 'Chemical'},
{id: 8, name: 'Agricultural'},
{id: 9, name: 'Electrical'}
];
let [major, setMajor] = React.useState(null);
let onSubmit = (value, key) => {
if (value) {
setMajor(value);
} else if (key) {
setMajor(options.find(o => o.id === key).name);
}
};
return (
<>
<SearchAutocomplete
label="Search engineering majors"
defaultItems={options}
onSubmit={onSubmit}>
{(item) => <Item>{item.name}</Item>}
</SearchAutocomplete>
<p>Results for: {major}</p>
</>
);
}
function Example() {
let options = [
{
id: 1,
name: 'Aerospace'
},
{
id: 2,
name: 'Mechanical'
},
{
id: 3,
name: 'Civil'
},
{
id: 4,
name: 'Biomedical'
},
{
id: 5,
name: 'Nuclear'
},
{
id: 6,
name: 'Industrial'
},
{
id: 7,
name: 'Chemical'
},
{
id: 8,
name:
'Agricultural'
},
{
id: 9,
name: 'Electrical'
}
];
let [major, setMajor] =
React.useState(null);
let onSubmit = (
value,
key
) => {
if (value) {
setMajor(value);
} else if (key) {
setMajor(
options.find(
(o) =>
o.id === key
).name
);
}
};
return (
<>
<SearchAutocomplete
label="Search engineering majors"
defaultItems={options}
onSubmit={onSubmit}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</SearchAutocomplete>
<p>
Results for:{' '}
{major}
</p>
</>
);
}
Custom filtering#
By default, useComboBoxState
uses the filter function passed to the defaultFilter
prop (in the above example, a
"contains" function from useFilter
). The filter function can be overridden by users of the SearchAutocomplete
component by
using the items
prop to control the filtered list. When items
is provided rather than defaultItems
, useComboBoxState
does no filtering of its own.
The following example makes the inputValue
controlled, and updates the filtered list that is passed to the items
prop when the input changes value.
function Example() {
let options = [
{id: 1, email: 'fake@email.com'},
{id: 2, email: 'anotherfake@email.com'},
{id: 3, email: 'bob@email.com'},
{id: 4, email: 'joe@email.com'},
{id: 5, email: 'yourEmail@email.com'},
{id: 6, email: 'valid@email.com'},
{id: 7, email: 'spam@email.com'},
{id: 8, email: 'newsletter@email.com'},
{id: 9, email: 'subscribe@email.com'}
];
let {startsWith} = useFilter({sensitivity: 'base'});
let [filterValue, setFilterValue] = React.useState('');
let filteredItems = React.useMemo(
() => options.filter((item) => startsWith(item.email, filterValue)),
[options, filterValue]
);
return (
<SearchAutocomplete
label="To:"
items={filteredItems}
inputValue={filterValue}
onInputChange={setFilterValue}>
{(item) => <Item>{item.email}</Item>}
</SearchAutocomplete>
);
}
function Example() {
let options = [
{ id: 1, email: 'fake@email.com' },
{ id: 2, email: 'anotherfake@email.com' },
{ id: 3, email: 'bob@email.com' },
{ id: 4, email: 'joe@email.com' },
{ id: 5, email: 'yourEmail@email.com' },
{ id: 6, email: 'valid@email.com' },
{ id: 7, email: 'spam@email.com' },
{ id: 8, email: 'newsletter@email.com' },
{ id: 9, email: 'subscribe@email.com' }
];
let { startsWith } = useFilter({ sensitivity: 'base' });
let [filterValue, setFilterValue] = React.useState('');
let filteredItems = React.useMemo(
() =>
options.filter((item) =>
startsWith(item.email, filterValue)
),
[options, filterValue]
);
return (
<SearchAutocomplete
label="To:"
items={filteredItems}
inputValue={filterValue}
onInputChange={setFilterValue}
>
{(item) => <Item>{item.email}</Item>}
</SearchAutocomplete>
);
}
function Example() {
let options = [
{
id: 1,
email:
'fake@email.com'
},
{
id: 2,
email:
'anotherfake@email.com'
},
{
id: 3,
email:
'bob@email.com'
},
{
id: 4,
email:
'joe@email.com'
},
{
id: 5,
email:
'yourEmail@email.com'
},
{
id: 6,
email:
'valid@email.com'
},
{
id: 7,
email:
'spam@email.com'
},
{
id: 8,
email:
'newsletter@email.com'
},
{
id: 9,
email:
'subscribe@email.com'
}
];
let { startsWith } =
useFilter({
sensitivity: 'base'
});
let [
filterValue,
setFilterValue
] = React.useState('');
let filteredItems =
React.useMemo(
() =>
options.filter((
item
) =>
startsWith(
item.email,
filterValue
)
),
[
options,
filterValue
]
);
return (
<SearchAutocomplete
label="To:"
items={filteredItems}
inputValue={filterValue}
onInputChange={setFilterValue}
>
{(item) => (
<Item>
{item.email}
</Item>
)}
</SearchAutocomplete>
);
}
Fully controlled#
The following example shows how you would create a controlled SearchAutocomplete, by controlling the input value (inputValue
)
and the autocomplete options (items
). By passing in inputValue
and items
to the SearchAutocomplete
you can control
exactly what your SearchAutocomplete should display. For example, note that the item filtering for the controlled SearchAutocomplete below now follows a "starts with"
filter strategy, accomplished by controlling the exact set of items available to the SearchAutocomplete whenever the input value updates.
function ControlledSearchAutocomplete() {
let optionList = [
{ name: 'Red Panda', id: '1' },
{ name: 'Cat', id: '2' },
{ name: 'Dog', id: '3' },
{ name: 'Aardvark', id: '4' },
{ name: 'Kangaroo', id: '5' },
{ name: 'Snake', id: '6' }
];
// Store SearchAutocomplete input value, selected option, open state, and items
// in a state tracker
let [fieldState, setFieldState] = React.useState({
inputValue: '',
items: optionList
});
// Implement custom filtering logic and control what items are
// available to the SearchAutocomplete.
let { startsWith } = useFilter({ sensitivity: 'base' });
// Specify how each of the SearchAutocomplete values should change when an
// option is selected from the list box
let onSubmit = (value, key) => {
setFieldState((prevState) => {
let selectedItem = prevState.items.find((option) => option.id === key);
return ({
inputValue: selectedItem?.name ?? '',
items: optionList.filter((item) =>
startsWith(item.name, selectedItem?.name ?? '')
)
});
});
};
// Specify how each of the SearchAutocomplete values should change when the input
// field is altered by the user
let onInputChange = (value) => {
setFieldState((prevState) => ({
inputValue: value,
items: optionList.filter((item) => startsWith(item.name, value))
}));
};
// Show entire list if user opens the menu manually
let onOpenChange = (isOpen, menuTrigger) => {
if (menuTrigger === 'manual' && isOpen) {
setFieldState((prevState) => ({
inputValue: prevState.inputValue,
items: optionList
}));
}
};
// Pass each controlled prop to useSearchAutocomplete along with their
// change handlers
return (
<SearchAutocomplete
label="Search Animals"
items={fieldState.items}
inputValue={fieldState.inputValue}
onOpenChange={onOpenChange}
onSubmit={onSubmit}
onInputChange={onInputChange}
>
{(item) => <Item>{item.name}</Item>}
</SearchAutocomplete>
);
}
<ControlledSearchAutocomplete />
function ControlledSearchAutocomplete() {
let optionList = [
{ name: 'Red Panda', id: '1' },
{ name: 'Cat', id: '2' },
{ name: 'Dog', id: '3' },
{ name: 'Aardvark', id: '4' },
{ name: 'Kangaroo', id: '5' },
{ name: 'Snake', id: '6' }
];
// Store SearchAutocomplete input value, selected option, open state, and items
// in a state tracker
let [fieldState, setFieldState] = React.useState({
inputValue: '',
items: optionList
});
// Implement custom filtering logic and control what items are
// available to the SearchAutocomplete.
let { startsWith } = useFilter({ sensitivity: 'base' });
// Specify how each of the SearchAutocomplete values should change when an
// option is selected from the list box
let onSubmit = (value, key) => {
setFieldState((prevState) => {
let selectedItem = prevState.items.find((option) =>
option.id === key
);
return ({
inputValue: selectedItem?.name ?? '',
items: optionList.filter((item) =>
startsWith(item.name, selectedItem?.name ?? '')
)
});
});
};
// Specify how each of the SearchAutocomplete values should change when the input
// field is altered by the user
let onInputChange = (value) => {
setFieldState((prevState) => ({
inputValue: value,
items: optionList.filter((item) =>
startsWith(item.name, value)
)
}));
};
// Show entire list if user opens the menu manually
let onOpenChange = (isOpen, menuTrigger) => {
if (menuTrigger === 'manual' && isOpen) {
setFieldState((prevState) => ({
inputValue: prevState.inputValue,
items: optionList
}));
}
};
// Pass each controlled prop to useSearchAutocomplete along with their
// change handlers
return (
<SearchAutocomplete
label="Search Animals"
items={fieldState.items}
inputValue={fieldState.inputValue}
onOpenChange={onOpenChange}
onSubmit={onSubmit}
onInputChange={onInputChange}
>
{(item) => <Item>{item.name}</Item>}
</SearchAutocomplete>
);
}
<ControlledSearchAutocomplete />
function ControlledSearchAutocomplete() {
let optionList = [
{
name: 'Red Panda',
id: '1'
},
{
name: 'Cat',
id: '2'
},
{
name: 'Dog',
id: '3'
},
{
name: 'Aardvark',
id: '4'
},
{
name: 'Kangaroo',
id: '5'
},
{
name: 'Snake',
id: '6'
}
];
// Store SearchAutocomplete input value, selected option, open state, and items
// in a state tracker
let [
fieldState,
setFieldState
] = React.useState({
inputValue: '',
items: optionList
});
// Implement custom filtering logic and control what items are
// available to the SearchAutocomplete.
let { startsWith } =
useFilter({
sensitivity: 'base'
});
// Specify how each of the SearchAutocomplete values should change when an
// option is selected from the list box
let onSubmit = (
value,
key
) => {
setFieldState(
(prevState) => {
let selectedItem =
prevState.items
.find(
(option) =>
option
.id ===
key
);
return ({
inputValue:
selectedItem
?.name ??
'',
items:
optionList
.filter(
(item) =>
startsWith(
item
.name,
selectedItem
?.name ??
''
)
)
});
}
);
};
// Specify how each of the SearchAutocomplete values should change when the input
// field is altered by the user
let onInputChange = (
value
) => {
setFieldState(
(prevState) => ({
inputValue:
value,
items: optionList
.filter(
(item) =>
startsWith(
item
.name,
value
)
)
})
);
};
// Show entire list if user opens the menu manually
let onOpenChange = (
isOpen,
menuTrigger
) => {
if (
menuTrigger ===
'manual' &&
isOpen
) {
setFieldState(
(prevState) => ({
inputValue:
prevState
.inputValue,
items:
optionList
})
);
}
};
// Pass each controlled prop to useSearchAutocomplete along with their
// change handlers
return (
<SearchAutocomplete
label="Search Animals"
items={fieldState
.items}
inputValue={fieldState
.inputValue}
onOpenChange={onOpenChange}
onSubmit={onSubmit}
onInputChange={onInputChange}
>
{(item) => (
<Item>
{item.name}
</Item>
)}
</SearchAutocomplete>
);
}
<ControlledSearchAutocomplete />
Menu trigger behavior#
useComboBoxState
supports three different menuTrigger
prop values:
input
(default): SearchAutocomplete menu opens when the user edits the input text.focus
: SearchAutocomplete menu opens when the user focuses the SearchAutocomplete input.manual
: SearchAutocomplete menu only opens when the user presses the trigger button or uses the arrow keys.
The example below has menuTrigger
set to focus
.
<SearchAutocomplete label="Search Animals" menuTrigger="focus">
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
label="Search Animals"
menuTrigger="focus"
>
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
label="Search Animals"
menuTrigger="focus"
>
<Item key="red panda">
Red Panda
</Item>
<Item key="cat">
Cat
</Item>
<Item key="dog">
Dog
</Item>
<Item key="aardvark">
Aardvark
</Item>
<Item key="kangaroo">
Kangaroo
</Item>
<Item key="snake">
Snake
</Item>
</SearchAutocomplete>
Disabled options#
You can disable specific options by providing an array of keys to useComboBoxState
via the disabledKeys
prop. This will prevent options with matching keys from being pressable and
receiving keyboard focus as shown in the example below. Note that you are responsible for the styling of disabled options.
<SearchAutocomplete label="Search Animals" disabledKeys={['cat', 'kangaroo']}>
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
label="Search Animals"
disabledKeys={['cat', 'kangaroo']}
>
<Item key="red panda">Red Panda</Item>
<Item key="cat">Cat</Item>
<Item key="dog">Dog</Item>
<Item key="aardvark">Aardvark</Item>
<Item key="kangaroo">Kangaroo</Item>
<Item key="snake">Snake</Item>
</SearchAutocomplete>
<SearchAutocomplete
label="Search Animals"
disabledKeys={[
'cat',
'kangaroo'
]}
>
<Item key="red panda">
Red Panda
</Item>
<Item key="cat">
Cat
</Item>
<Item key="dog">
Dog
</Item>
<Item key="aardvark">
Aardvark
</Item>
<Item key="kangaroo">
Kangaroo
</Item>
<Item key="snake">
Snake
</Item>
</SearchAutocomplete>
Asynchronous loading#
This example uses the useAsyncList hook to handle asynchronous loading and filtering of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data.
import {useAsyncList} from '@react-stately/data';
function AsyncLoadingExample() {
let list = useAsyncList({
async load({signal, filterText}) {
let res = await fetch(
`https://swapi.py4e.com/api/people/?search= `,
{signal}
);
let json = await res.json();
return {
items: json.results
};
}
});
return (
<SearchAutocomplete
label="Search Star Wars Characters"
items={list.items}
inputValue={list.filterText}
onInputChange={list.setFilterText}>
{(item) => <Item key={item.name}>{item.name}</Item>}
</SearchAutocomplete>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncLoadingExample() {
let list = useAsyncList({
async load({ signal, filterText }) {
let res = await fetch(
`https://swapi.py4e.com/api/people/?search= `,
{ signal }
);
let json = await res.json();
return {
items: json.results
};
}
});
return (
<SearchAutocomplete
label="Search Star Wars Characters"
items={list.items}
inputValue={list.filterText}
onInputChange={list.setFilterText}
>
{(item) => <Item key={item.name}>{item.name}</Item>}
</SearchAutocomplete>
);
}
import {useAsyncList} from '@react-stately/data';
function AsyncLoadingExample() {
let list =
useAsyncList({
async load(
{
signal,
filterText
}
) {
let res =
await fetch(
`https://swapi.py4e.com/api/people/?search= `,
{ signal }
);
let json =
await res
.json();
return {
items:
json.results
};
}
});
return (
<SearchAutocomplete
label="Search Star Wars Characters"
items={list.items}
inputValue={list
.filterText}
onInputChange={list
.setFilterText}
>
{(item) => (
<Item
key={item.name}
>
{item.name}
</Item>
)}
</SearchAutocomplete>
);
}
Links#
By default, interacting with an item in a SearchAutocomplete updates the input value. Alternatively, items may be links to another page or website. This can be achieved by passing the href
prop to the <Item>
component. Interacting with link items navigates to the provided URL and does not update the input value. See the links section in the useListBox
docs for details on how to support this.
Internationalization#
useSearchAutocomplete
handles some aspects of internationalization automatically.
For example, the item focus, count, and selection VoiceOver announcements are localized.
You are responsible for localizing all labels and option
content that is passed into the autocomplete.