useComboBox
Provides the behavior and accessibility implementation for a combobox component. A combobox combines a text entry with a picker menu, allowing users to filter longer lists to only the selections matching a query.
| install | yarn add @react-aria/combobox |
|---|---|
| version | 3.0.0-alpha.0 |
| usage | import {useComboBox} from '@react-aria/combobox' |
API#
useComboBox<T>(
(props: AriaComboBoxProps<T>,
, state: ComboBoxState<T>
)): ComboBoxAriaFeatures#
There is no native element to implement a combobox in HTML. useComboBox helps achieve accessible combobox components that can
be styled as needed.
- Exposed to assistive technology as a combobox
- Support for selecting a single option
- Support for disabled options
- Support for sections
- Labeling support for accessibility
- Support for mouse, touch, and keyboard interactions
- Keyboard support for opening the combobox menu using the arrow keys, including automatically focusing the first or last item accordingly
- Virtual focus management for combobox menu option navigation
- VoiceOver announcement enhancements for option focusing, filtering, and selection
Anatomy#
A combobox consists of a label, an input which displays the current value, a listbox popup, and a button
used to toggle the listbox popup open state. Users can type within the input to filter the available options
within the listbox popup. The listbox 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 combobox trigger button. useComboBox handles exposing
the correct ARIA attributes for accessibility for each of the components comprising the combobox. It should be combined
with useListBox, which handles the implementation of the popup listbox,
and useButton which handles the button press handlers returned by useComboBox.
useComboBox returns props that you should spread onto the appropriate elements:
| Name | Type | Description |
buttonProps | AriaButtonProps | Props for the combobox menu trigger button. |
inputProps | InputHTMLAttributes<HTMLInputElement> | Props for the combobox input element. |
listBoxProps | HTMLAttributes<HTMLElement> | Props for the combobox menu. |
labelProps | HTMLAttributes<HTMLElement> | Props for the combobox label element. |
State is managed by the useComboBoxState hook from @react-stately/select.
The state object should be passed as an option to useComboBox.
If the combobox 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 combobox 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 combobox is focused, and the current input value.
For more information about selection, see Selection.
Example#
This example uses an <input> element for the combobox text input and a <button> element for the combobox menu 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 combobox list can be filtered based on the option text and the current combobox input text.
The listbox popup uses useListBox
and useOption to render the
list of options. A hidden <DismissButton>
is added at the start and end of the popup to allow screen reader users to dismiss the popup. Note that shouldUseVirtualFocus is passed to useListBox
and useOption so that browser focus remains within the combobox <input> element even when interacting with the combobox menu 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});
// Get props for child elements from useComboBox
let triggerRef = ReactuseRef();
let inputRef = ReactuseRef();
let listBoxRef = ReactuseRef();
let popoverRef = ReactuseRef();
let {
buttonProps: triggerProps
inputProps
listBoxProps
labelProps
} = useComboBox(
{
...props
inputRef
buttonRef: triggerRef
listBoxRef
popoverRef
menuTrigger: 'input'
}
state
);
// Get props for the combobox trigger button based on the button props from useComboBox
let {buttonProps} = useButton(triggerProps triggerRef);
return (
<div style={position: 'relative' display: 'inline-block'}>
<div ...labelProps>propslabel</div>
<input
...inputProps
ref=inputRef
style={
borderRight: 0
borderBottomRightRadius: 0
borderTopRightRadius: 0
}
/>
<button
...buttonProps
ref=triggerRef
style={
height: '21px'
borderBottomLeftRadius: 0
borderTopLeftRadius: 0
}>
<span aria-hidden="true" style={paddingLeft: 5}>
▼
</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>
);
}
function ListBoxPopup(props) {
let {
popoverRef
listBoxRef
state
shouldUseVirtualFocus
...otherProps
} = props;
// Get props for the listbox. Prevent focus moving to listbox 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 hidden <DismissButton> components at the start and end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<div ...overlayProps ref=popoverRef>
<DismissButton onDismiss=() => stateclose() />
<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 recieving true focus via shouldUseVirtualFocus.
let {optionProps} = useOption(
{
key: itemkey
isDisabled
isSelected
shouldSelectOnPressUp: true
shouldFocusOnHover: true
shouldUseVirtualFocus
}
state
ref
);
return (
<li
...optionProps
ref=ref
style={
background: isSelected
? 'blueviolet'
: isFocused
? 'gray'
: 'transparent'
color: isSelected || isFocused ? 'white' : 'black'
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
});
// Get props for child elements from useComboBox
let triggerRef = ReactuseRef();
let inputRef = ReactuseRef();
let listBoxRef = ReactuseRef();
let popoverRef = ReactuseRef();
let {
buttonProps: triggerProps
inputProps
listBoxProps
labelProps
} = useComboBox(
{
...props
inputRef
buttonRef: triggerRef
listBoxRef
popoverRef
menuTrigger: 'input'
}
state
);
// Get props for the combobox trigger button based on the button props from useComboBox
let {buttonProps} = useButton(triggerProps triggerRef);
return (
<div
style={
position: 'relative'
display: 'inline-block'
}>
<div ...labelProps>propslabel</div>
<input
...inputProps
ref=inputRef
style={
borderRight: 0
borderBottomRightRadius: 0
borderTopRightRadius: 0
}
/>
<button
...buttonProps
ref=triggerRef
style={
height: '21px'
borderBottomLeftRadius: 0
borderTopLeftRadius: 0
}>
<span aria-hidden="true" style={paddingLeft: 5}>
▼
</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>
);
}
function ListBoxPopup(props) {
let {
popoverRef
listBoxRef
state
shouldUseVirtualFocus
...otherProps
} = props;
// Get props for the listbox. Prevent focus moving to listbox 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 hidden <DismissButton> components at the start and end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<div ...overlayProps ref=popoverRef>
<DismissButton onDismiss=() => stateclose() />
<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 recieving true focus via shouldUseVirtualFocus.
let {optionProps} = useOption(
{
key: itemkey
isDisabled
isSelected
shouldSelectOnPressUp: true
shouldFocusOnHover: true
shouldUseVirtualFocus
}
state
ref
);
return (
<li
...optionProps
ref=ref
style={
background: isSelected
? 'blueviolet'
: isFocused
? 'gray'
: 'transparent'
color: isSelected || isFocused ? 'white' : 'black'
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
}
);
// Get props for child elements from useComboBox
let triggerRef = ReactuseRef();
let inputRef = ReactuseRef();
let listBoxRef = ReactuseRef();
let popoverRef = ReactuseRef();
let {
buttonProps: triggerProps
inputProps
listBoxProps
labelProps
} = useComboBox(
{
...props
inputRef
buttonRef: triggerRef
listBoxRef
popoverRef
menuTrigger:
'input'
}
state
);
// Get props for the combobox trigger button based on the button props from useComboBox
let {
buttonProps
} = useButton(
triggerProps
triggerRef
);
return (
<div
style={
position:
'relative'
display:
'inline-block'
}>
<div
...labelProps>
propslabel
</div>
<input
...inputProps
ref=inputRef
style={
borderRight: 0
borderBottomRightRadius: 0
borderTopRightRadius: 0
}
/>
<button
...buttonProps
ref=triggerRef
style={
height: '21px'
borderBottomLeftRadius: 0
borderTopLeftRadius: 0
}>
<span
aria-hidden="true"
style={
paddingLeft: 5
}>
▼
</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>
);
}
function ListBoxPopup(
props
) {
let {
popoverRef
listBoxRef
state
shouldUseVirtualFocus
...otherProps
} = props;
// Get props for the listbox. Prevent focus moving to listbox 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 hidden <DismissButton> components at the start and end of the list
// to allow screen reader users to dismiss the popup easily.
return (
<div
...overlayProps
ref=popoverRef>
<DismissButton
onDismiss=() =>
stateclose()
/>
<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 recieving true focus via shouldUseVirtualFocus.
let {
optionProps
} = useOption(
{
key: itemkey
isDisabled
isSelected
shouldSelectOnPressUp: true
shouldFocusOnHover: true
shouldUseVirtualFocus
}
state
ref
);
return (
<li
...optionProps
ref=ref
style={
background: isSelected
? 'blueviolet'
: isFocused
? 'gray'
: 'transparent'
color:
isSelected ||
isFocused
? 'white'
: 'black'
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>Internationalization#
useComboBox and useListBox handle some aspects of internationalization automatically.
For example, the item focus, count, and selection VoiceOver announcements are formatted to match the current locale.
You are responsible for localizing all labels and option
content that is passed into the select.
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.