useComboBox
Provides the behavior and accessibility implementation for a combo box component. A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query.
install | yarn add @react-aria/combobox |
---|---|
version | 3.0.0-rc.0 |
usage | import {useComboBox} from '@react-aria/combobox' |
API#
useComboBox<T>(
(props: AriaComboBoxProps<T>,
, state: ComboBoxState<T>
)): ComboBoxAria
Features#
A combo box can be built using the <datalist> HTML element, but this is very limited in functionality and difficult to style.
useComboBox
helps achieve accessible combo box 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
- 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 combo box 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 combo box 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 combo box consists of a label, an input which displays the current value, a list box popup, and an optional button
used to toggle the list box popup open state. Users can type within the input to filter the available options
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 useComboBox
, or by clicking or touching the button. useComboBox
handles exposing
the correct ARIA attributes for accessibility for each of the elements comprising the combo box. It should be combined
with useListBox, which handles the implementation of the popup list box,
and useButton which handles the button press interactions.
useComboBox
returns props that you should spread onto the appropriate elements:
Name | Type | Description |
buttonProps | AriaButtonProps | Props for the combo box menu trigger button. |
inputProps | InputHTMLAttributes<HTMLInputElement> | Props for the combo box input element. |
listBoxProps | HTMLAttributes<HTMLElement> | Props for the combo box menu. |
labelProps | HTMLAttributes<HTMLElement> | Props for the combo box label element. |
State is managed by the useComboBoxState
hook from @react-stately/combobox
.
The state object should be passed as an option to useComboBox
.
If the combo box does not have a visible label, an aria-label
or aria-labelledby
prop must be passed instead to
identify it to assistive technology.
State management#
useComboBox
requires knowledge of the options in the combo box 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 combo box is focused, and the current input value.
For more information about selection, see Selection.
Example#
This example uses an <input>
element for the combo box text input and a <button>
element for the list box popup trigger. A <span>
is included within the <button>
to display the dropdown arrow icon (hidden from screen readers with aria-hidden
).
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 uses useListBox
and useOption
to render the
list of options. A hidden <DismissButton
>
is added at the end of the popup to allow screen reader users to dismiss the popup easily, without navigating back through the entire list.
Note that shouldUseVirtualFocus
is passed to useListBox
and useOption
so that browser focus remains within the
<input>
element even when interacting with the list box options.
This example does not do any advanced popover positioning or portaling to escape its visual container.
See useOverlayTrigger for an example of how to implement this
using useOverlayPosition
.
In addition, see useListBox for examples of sections (option groups), and more complex options.
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useFilter} from '@react-aria/i18n';
import {useListBox useOption} from '@react-aria/listbox';
import {useOverlay DismissButton} from '@react-aria/overlays';
function ComboBox(props) {
// Get a basic "contains" filter function for input value
// and option text comparison
let {contains} = useFilter({sensitivity: 'base'});
// Create state based on the incoming props and the filter function
let state = useComboBoxState({...props defaultFilter: contains});
let triggerRef = ReactuseRef();
let inputRef = ReactuseRef();
let listBoxRef = ReactuseRef();
let popoverRef = ReactuseRef();
// Get props for child elements from useComboBox
let {
buttonProps: triggerProps
inputProps
listBoxProps
labelProps
} = useComboBox(
{
...props
inputRef
buttonRef: triggerRef
listBoxRef
popoverRef
menuTrigger: 'input'
}
state
);
// Get props for the trigger button based on the
// button props from useComboBox
let {buttonProps} = useButton(triggerProps triggerRef);
return (
<div style={display: 'inline-flex' flexDirection: 'column'}>
<label ...labelProps> propslabel</label>
<div style={position: 'relative' display: 'inline-block'}>
<input
...inputProps
ref= inputRef
style={
height: 22
boxSizing: 'border-box'
marginRight: 0
}
/>
<button
...buttonProps
ref= triggerRef
style={
height: 22
marginLeft: 0
}>
<span aria-hidden="true" style={padding: '0 2px'}>
▼
</span>
</button>
stateisOpen && (
<ListBoxPopup
...listBoxProps
// Use virtual focus to get aria-activedescendant tracking and
// ensure focus doesn't leave the input field
shouldUseVirtualFocus
listBoxRef= listBoxRef
popoverRef= popoverRef
state= state
/>
)
</div>
</div>
);
}
function ListBoxPopup(props) {
let {
popoverRef
listBoxRef
state
shouldUseVirtualFocus
...otherProps
} = props;
// Get props for the list box.
// Prevent focus moving to list box via shouldUseVirtualFocus
let {listBoxProps} = useListBox(
{
autoFocus: statefocusStrategy
disallowEmptySelection: true
shouldUseVirtualFocus
...otherProps
}
state
listBoxRef
);
// Handle events that should cause the popup to close,
// e.g. blur, clicking outside, or pressing the escape key.
let {overlayProps} = useOverlay(
{
onClose: () => stateclose()
shouldCloseOnBlur: true
isOpen: stateisOpen
isDismissable: true
}
popoverRef
);
// Add a hidden <DismissButton> component at the end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<div ...overlayProps ref= popoverRef>
<ul
...mergeProps(listBoxProps otherProps)
ref= listBoxRef
style={
position: 'absolute'
width: '100%'
margin: '4px 0 0 0'
padding: 0
listStyle: 'none'
border: '1px solid gray'
background: 'lightgray'
}>
[...statecollection]map((item) => (
<Option
shouldUseVirtualFocus
key= itemkey
item= item
state= state
/>
))
</ul>
<DismissButton onDismiss=() => stateclose() />
</div>
);
}
function Option({item state shouldUseVirtualFocus}) {
let ref = ReactuseRef();
let isDisabled = statedisabledKeyshas(itemkey);
let isSelected = stateselectionManagerisSelected(itemkey);
// Track focus via focusedKey state instead of with focus event listeners
// since focus never leaves the text input in a ComboBox
let isFocused = stateselectionManagerfocusedKey === itemkey;
// Get props for the option element.
// Prevent options from receiving browser focus via shouldUseVirtualFocus.
let {optionProps} = useOption(
{
key: itemkey
isDisabled
isSelected
shouldSelectOnPressUp: true
shouldFocusOnHover: true
shouldUseVirtualFocus
}
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'
}>
itemrendered
</li>
);
}
<ComboBox label="Favorite Animal">
<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>
</ComboBox>
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useFilter} from '@react-aria/i18n';
import {useListBox useOption} from '@react-aria/listbox';
import {
useOverlay
DismissButton
} from '@react-aria/overlays';
function ComboBox(props) {
// Get a basic "contains" filter function for input value
// and option text comparison
let {contains} = useFilter({sensitivity: 'base'});
// Create state based on the incoming props and the filter function
let state = useComboBoxState({
...props
defaultFilter: contains
});
let triggerRef = ReactuseRef();
let inputRef = ReactuseRef();
let listBoxRef = ReactuseRef();
let popoverRef = ReactuseRef();
// Get props for child elements from useComboBox
let {
buttonProps: triggerProps
inputProps
listBoxProps
labelProps
} = useComboBox(
{
...props
inputRef
buttonRef: triggerRef
listBoxRef
popoverRef
menuTrigger: 'input'
}
state
);
// Get props for the trigger button based on the
// button props from useComboBox
let {buttonProps} = useButton(triggerProps triggerRef);
return (
<div
style={
display: 'inline-flex'
flexDirection: 'column'
}>
<label ...labelProps> propslabel</label>
<div
style={
position: 'relative'
display: 'inline-block'
}>
<input
...inputProps
ref= inputRef
style={
height: 22
boxSizing: 'border-box'
marginRight: 0
}
/>
<button
...buttonProps
ref= triggerRef
style={
height: 22
marginLeft: 0
}>
<span
aria-hidden="true"
style={padding: '0 2px'}>
▼
</span>
</button>
stateisOpen && (
<ListBoxPopup
...listBoxProps
// Use virtual focus to get aria-activedescendant tracking and
// ensure focus doesn't leave the input field
shouldUseVirtualFocus
listBoxRef= listBoxRef
popoverRef= popoverRef
state= state
/>
)
</div>
</div>
);
}
function ListBoxPopup(props) {
let {
popoverRef
listBoxRef
state
shouldUseVirtualFocus
...otherProps
} = props;
// Get props for the list box.
// Prevent focus moving to list box via shouldUseVirtualFocus
let {listBoxProps} = useListBox(
{
autoFocus: statefocusStrategy
disallowEmptySelection: true
shouldUseVirtualFocus
...otherProps
}
state
listBoxRef
);
// Handle events that should cause the popup to close,
// e.g. blur, clicking outside, or pressing the escape key.
let {overlayProps} = useOverlay(
{
onClose: () => stateclose()
shouldCloseOnBlur: true
isOpen: stateisOpen
isDismissable: true
}
popoverRef
);
// Add a hidden <DismissButton> component at the end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<div ...overlayProps ref= popoverRef>
<ul
...mergeProps(listBoxProps otherProps)
ref= listBoxRef
style={
position: 'absolute'
width: '100%'
margin: '4px 0 0 0'
padding: 0
listStyle: 'none'
border: '1px solid gray'
background: 'lightgray'
}>
[...statecollection]map((item) => (
<Option
shouldUseVirtualFocus
key= itemkey
item= item
state= state
/>
))
</ul>
<DismissButton onDismiss=() => stateclose() />
</div>
);
}
function Option({item state shouldUseVirtualFocus}) {
let ref = ReactuseRef();
let isDisabled = statedisabledKeyshas(itemkey);
let isSelected = stateselectionManagerisSelected(
itemkey
);
// Track focus via focusedKey state instead of with focus event listeners
// since focus never leaves the text input in a ComboBox
let isFocused =
stateselectionManagerfocusedKey === itemkey;
// Get props for the option element.
// Prevent options from receiving browser focus via shouldUseVirtualFocus.
let {optionProps} = useOption(
{
key: itemkey
isDisabled
isSelected
shouldSelectOnPressUp: true
shouldFocusOnHover: true
shouldUseVirtualFocus
}
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'
}>
itemrendered
</li>
);
}
<ComboBox label="Favorite Animal">
<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>
</ComboBox>
import {Item} from '@react-stately/collections';
import {mergeProps} from '@react-aria/utils';
import {useButton} from '@react-aria/button';
import {useComboBoxState} from '@react-stately/combobox';
import {useFilter} from '@react-aria/i18n';
import {
useListBox
useOption
} from '@react-aria/listbox';
import {
useOverlay
DismissButton
} from '@react-aria/overlays';
function ComboBox(
props
) {
// Get a basic "contains" filter function for input value
// and option text comparison
let {
contains
} = useFilter({
sensitivity: 'base'
});
// Create state based on the incoming props and the filter function
let state = useComboBoxState(
{
...props
defaultFilter: contains
}
);
let triggerRef = ReactuseRef();
let inputRef = ReactuseRef();
let listBoxRef = ReactuseRef();
let popoverRef = ReactuseRef();
// Get props for child elements from useComboBox
let {
buttonProps: triggerProps
inputProps
listBoxProps
labelProps
} = useComboBox(
{
...props
inputRef
buttonRef: triggerRef
listBoxRef
popoverRef
menuTrigger:
'input'
}
state
);
// Get props for the trigger button based on the
// button props from useComboBox
let {
buttonProps
} = useButton(
triggerProps
triggerRef
);
return (
<div
style={
display:
'inline-flex'
flexDirection:
'column'
}>
<label
...labelProps>
propslabel
</label>
<div
style={
position:
'relative'
display:
'inline-block'
}>
<input
...inputProps
ref= inputRef
style={
height: 22
boxSizing:
'border-box'
marginRight: 0
}
/>
<button
...buttonProps
ref=
triggerRef
style={
height: 22
marginLeft: 0
}>
<span
aria-hidden="true"
style={
padding:
'0 2px'
}>
▼
</span>
</button>
stateisOpen && (
<ListBoxPopup
...listBoxProps
// Use virtual focus to get aria-activedescendant tracking and
// ensure focus doesn't leave the input field
shouldUseVirtualFocus
listBoxRef=
listBoxRef
popoverRef=
popoverRef
state= state
/>
)
</div>
</div>
);
}
function ListBoxPopup(
props
) {
let {
popoverRef
listBoxRef
state
shouldUseVirtualFocus
...otherProps
} = props;
// Get props for the list box.
// Prevent focus moving to list box via shouldUseVirtualFocus
let {
listBoxProps
} = useListBox(
{
autoFocus:
statefocusStrategy
disallowEmptySelection: true
shouldUseVirtualFocus
...otherProps
}
state
listBoxRef
);
// Handle events that should cause the popup to close,
// e.g. blur, clicking outside, or pressing the escape key.
let {
overlayProps
} = useOverlay(
{
onClose: () =>
stateclose()
shouldCloseOnBlur: true
isOpen:
stateisOpen
isDismissable: true
}
popoverRef
);
// Add a hidden <DismissButton> component at the end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<div
...overlayProps
ref= popoverRef>
<ul
...mergeProps(
listBoxProps
otherProps
)
ref= listBoxRef
style={
position:
'absolute'
width: '100%'
margin:
'4px 0 0 0'
padding: 0
listStyle:
'none'
border:
'1px solid gray'
background:
'lightgray'
}>
[
...statecollection
]map((item) => (
<Option
shouldUseVirtualFocus
key=
itemkey
item= item
state= state
/>
))
</ul>
<DismissButton
onDismiss=() =>
stateclose()
/>
</div>
);
}
function Option({
item
state
shouldUseVirtualFocus
}) {
let ref = ReactuseRef();
let isDisabled = statedisabledKeyshas(
itemkey
);
let isSelected = stateselectionManagerisSelected(
itemkey
);
// Track focus via focusedKey state instead of with focus event listeners
// since focus never leaves the text input in a ComboBox
let isFocused =
state
selectionManager
focusedKey ===
itemkey;
// Get props for the option element.
// Prevent options from receiving browser focus via shouldUseVirtualFocus.
let {
optionProps
} = useOption(
{
key: itemkey
isDisabled
isSelected
shouldSelectOnPressUp: true
shouldFocusOnHover: true
shouldUseVirtualFocus
}
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'
}>
itemrendered
</li>
);
}
<ComboBox label="Favorite Animal">
<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>
</ComboBox>
Usage#
The following examples show how to use the ComboBox component created in the above example.
Uncontrolled#
The following example shows how you would create an uncontrolled ComboBox. The input value, selected option, and open state is completely
uncontrolled, with the default input text set by the defaultInputValue
prop.
<ComboBox label="Favorite Animal" defaultInputValue="red">
<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>
</ComboBox>
<ComboBox label="Favorite Animal" defaultInputValue="red">
<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>
</ComboBox>
<ComboBox
label="Favorite Animal"
defaultInputValue="red">
<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>
</ComboBox>
Dynamic collections#
ComboBox 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 ComboBox using the defaultItems
prop. Each item accepts a key
prop, which
is passed to the onSelectionChange
handler to identify the selected item. Alternatively, if the item objects contain an id
property,
as shown in the example below, then this is used automatically and a key
prop is not required.
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 [majorId setMajorId] = ReactuseState();
return (
<>
<ComboBox
label="Pick a engineering major"
defaultItems= options
onSelectionChange= setMajorId>
(item) => <Item> itemname</Item>
</ComboBox>
<p>Selected topic id: majorId</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 [majorId setMajorId] = ReactuseState();
return (
<>
<ComboBox
label="Pick a engineering major"
defaultItems= options
onSelectionChange= setMajorId>
(item) => <Item> itemname</Item>
</ComboBox>
<p>Selected topic id: majorId</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 [
majorId
setMajorId
] = ReactuseState();
return (
<>
<ComboBox
label="Pick a engineering major"
defaultItems=
options
onSelectionChange=
setMajorId
>
(item) => (
<Item>
itemname
</Item>
)
</ComboBox>
<p>
Selected topic
id: majorId
</p>
</>
);
}
Allow custom values#
By default, useComboBoxState
doesn't allow users to specify a value that doesn't exist in the list of options and will revert the input value to
the current selected value on blur. By specifying allowsCustomValue
, this behavior is suppressed and the user is free to enter
any value within the field.
<ComboBox label="Favorite Animal" allowsCustomValue>
<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>
</ComboBox>
<ComboBox label="Favorite Animal" allowsCustomValue>
<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>
</ComboBox>
<ComboBox
label="Favorite Animal"
allowsCustomValue>
<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>
</ComboBox>
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 ComboBox
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] = ReactuseState('');
let filteredItems = ReactuseMemo(
() => optionsfilter((item) => startsWith(itememail filterValue))
[options filterValue]
);
return (
<ComboBox
label="To:"
items= filteredItems
inputValue= filterValue
onInputChange= setFilterValue
placeholder="Enter recipient email"
allowsCustomValue>
(item) => <Item> itememail</Item>
</ComboBox>
);
}
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] = ReactuseState('');
let filteredItems = ReactuseMemo(
() =>
optionsfilter((item) =>
startsWith(itememail filterValue)
)
[options filterValue]
);
return (
<ComboBox
label="To:"
items= filteredItems
inputValue= filterValue
onInputChange= setFilterValue
placeholder="Enter recipient email"
allowsCustomValue>
(item) => <Item> itememail</Item>
</ComboBox>
);
}
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
] = ReactuseState('');
let filteredItems = ReactuseMemo(
() =>
optionsfilter(
(item) =>
startsWith(
itememail
filterValue
)
)
[
options
filterValue
]
);
return (
<ComboBox
label="To:"
items=
filteredItems
inputValue=
filterValue
onInputChange=
setFilterValue
placeholder="Enter recipient email"
allowsCustomValue>
(item) => (
<Item>
itememail
</Item>
)
</ComboBox>
);
}
Fully controlled#
The following example shows how you would create a controlled ComboBox, controlling everything from the selected value (selectedKey
)
to the combobox options (items
). By passing in inputValue
, selectedKey
, and items
to the ComboBox
you can control
exactly what your ComboBox should display. For example, note that the item filtering for the controlled ComboBox below now follows a "starts with"
filter strategy, accomplished by controlling the exact set of items available to the ComboBox whenever the input value updates.
It is important to note that you don't have to control every single aspect of a ComboBox. If you decide to only control a single property of the
ComboBox, be sure to provide the change handler for that prop as well e.g. controlling selectedKey
would require onSelectionChange
to be passed to useComboBox
as well.
function ControlledComboBox() {
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 ComboBox input value, selected option, open state, and items
// in a state tracker
let [fieldState setFieldState] = ReactuseState({
selectedKey: ''
inputValue: ''
items: optionList
});
// Implement custom filtering logic and control what items are
// available to the ComboBox.
let {startsWith} = useFilter({sensitivity: 'base'});
// Specify how each of the ComboBox values should change when an
// option is selected from the list box
let onSelectionChange = (key) => {
setFieldState((prevState) => {
let selectedItem = prevStateitemsfind((option) => optionid === key);
return {
inputValue: selectedItem?name ?? ''
selectedKey: key
items: optionListfilter((item) =>
startsWith(itemname selectedItem?name ?? '')
)
};
});
};
// Specify how each of the ComboBox values should change when the input
// field is altered by the user
let onInputChange = (value) => {
setFieldState((prevState) => ({
inputValue: value
selectedKey: value === '' ? null : prevStateselectedKey
items: optionListfilter((item) => startsWith(itemname value))
}));
};
// Show entire list if user opens the menu manually
let onOpenChange = (isOpen menuTrigger) => {
if (menuTrigger === 'manual' && isOpen) {
setFieldState((prevState) => ({
inputValue: prevStateinputValue
selectedKey: prevStateselectedKey
items: optionList
}));
}
};
// Pass each controlled prop to useComboBox along with their
// change handlers
return (
<ComboBox
label="Favorite Animal"
items= fieldStateitems
selectedKey= fieldStateselectedKey
inputValue= fieldStateinputValue
onOpenChange= onOpenChange
onSelectionChange= onSelectionChange
onInputChange= onInputChange>
(item) => <Item> itemname</Item>
</ComboBox>
);
}
<ControlledComboBox />
function ControlledComboBox() {
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 ComboBox input value, selected option, open state, and items
// in a state tracker
let [fieldState setFieldState] = ReactuseState({
selectedKey: ''
inputValue: ''
items: optionList
});
// Implement custom filtering logic and control what items are
// available to the ComboBox.
let {startsWith} = useFilter({sensitivity: 'base'});
// Specify how each of the ComboBox values should change when an
// option is selected from the list box
let onSelectionChange = (key) => {
setFieldState((prevState) => {
let selectedItem = prevStateitemsfind(
(option) => optionid === key
);
return {
inputValue: selectedItem?name ?? ''
selectedKey: key
items: optionListfilter((item) =>
startsWith(itemname selectedItem?name ?? '')
)
};
});
};
// Specify how each of the ComboBox values should change when the input
// field is altered by the user
let onInputChange = (value) => {
setFieldState((prevState) => ({
inputValue: value
selectedKey:
value === '' ? null : prevStateselectedKey
items: optionListfilter((item) =>
startsWith(itemname value)
)
}));
};
// Show entire list if user opens the menu manually
let onOpenChange = (isOpen menuTrigger) => {
if (menuTrigger === 'manual' && isOpen) {
setFieldState((prevState) => ({
inputValue: prevStateinputValue
selectedKey: prevStateselectedKey
items: optionList
}));
}
};
// Pass each controlled prop to useComboBox along with their
// change handlers
return (
<ComboBox
label="Favorite Animal"
items= fieldStateitems
selectedKey= fieldStateselectedKey
inputValue= fieldStateinputValue
onOpenChange= onOpenChange
onSelectionChange= onSelectionChange
onInputChange= onInputChange>
(item) => <Item> itemname</Item>
</ComboBox>
);
}
<ControlledComboBox />
function ControlledComboBox() {
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 ComboBox input value, selected option, open state, and items
// in a state tracker
let [
fieldState
setFieldState
] = ReactuseState({
selectedKey: ''
inputValue: ''
items: optionList
});
// Implement custom filtering logic and control what items are
// available to the ComboBox.
let {
startsWith
} = useFilter({
sensitivity: 'base'
});
// Specify how each of the ComboBox values should change when an
// option is selected from the list box
let onSelectionChange = (
key
) => {
setFieldState(
(prevState) => {
let selectedItem = prevStateitemsfind(
(option) =>
optionid ===
key
);
return {
inputValue:
selectedItem?name ??
''
selectedKey: key
items: optionListfilter(
(item) =>
startsWith(
itemname
selectedItem?name ??
''
)
)
};
}
);
};
// Specify how each of the ComboBox values should change when the input
// field is altered by the user
let onInputChange = (
value
) => {
setFieldState(
(prevState) => ({
inputValue: value
selectedKey:
value === ''
? null
: prevStateselectedKey
items: optionListfilter(
(item) =>
startsWith(
itemname
value
)
)
})
);
};
// Show entire list if user opens the menu manually
let onOpenChange = (
isOpen
menuTrigger
) => {
if (
menuTrigger ===
'manual' &&
isOpen
) {
setFieldState(
(prevState) => ({
inputValue:
prevStateinputValue
selectedKey:
prevStateselectedKey
items: optionList
})
);
}
};
// Pass each controlled prop to useComboBox along with their
// change handlers
return (
<ComboBox
label="Favorite Animal"
items=
fieldStateitems
selectedKey=
fieldStateselectedKey
inputValue=
fieldStateinputValue
onOpenChange=
onOpenChange
onSelectionChange=
onSelectionChange
onInputChange=
onInputChange
>
(item) => (
<Item>
itemname
</Item>
)
</ComboBox>
);
}
<ControlledComboBox />
Menu trigger behavior#
useComboBoxState
supports three different menuTrigger
prop values:
input
(default): ComboBox menu opens when the user edits the input text.focus
: ComboBox menu opens when the user focuses the ComboBox input.manual
: ComboBox menu only opens when the user presses the trigger button or uses the arrow keys.
The example below has menuTrigger
set to focus
.
<ComboBox label="Favorite Animal" 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>
</ComboBox>
<ComboBox label="Favorite Animal" 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>
</ComboBox>
<ComboBox
label="Favorite Animal"
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>
</ComboBox>
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.
<ComboBox label="Favorite Animal" 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>
</ComboBox>
<ComboBox
label="Favorite Animal"
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>
</ComboBox>
<ComboBox
label="Favorite Animal"
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>
</ComboBox>
Internationalization#
useComboBox
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 combo box.
RTL#
In right-to-left languages, the ComboBox should be mirrored. The trigger button should be on the left, and the input element should be on the right. In addition, the content of ComboBox options should flip. Ensure that your CSS accounts for this.