The useCombobox hook provides functionality for building accessible combobox/autocomplete components with input field and filtering capabilities. It's ideal for scenarios where users need to type to filter options or enter custom values.
Creates a combobox component with input field, filtering, and full accessibility features.
/**
* Hook for building accessible combobox/autocomplete components
* @param props - Configuration options for the combobox component
* @returns Object containing state, actions, and prop getters
*/
function useCombobox<Item>(props: UseComboboxProps<Item>): UseComboboxReturnValue<Item>;
interface UseComboboxProps<Item> {
/** Array of items to display in the combobox dropdown */
items: Item[];
/** Function to convert an item to its string representation */
itemToString?: (item: Item | null) => string;
/** Function to generate a unique key for each item */
itemToKey?: (item: Item | null) => any;
/** Function to determine if an item is disabled */
isItemDisabled?: (item: Item, index: number) => boolean;
/** Currently highlighted item index */
highlightedIndex?: number;
/** Initial highlighted index when component mounts */
initialHighlightedIndex?: number;
/** Default highlighted index for uncontrolled usage */
defaultHighlightedIndex?: number;
/** Whether the dropdown is open */
isOpen?: boolean;
/** Initial open state when component mounts */
initialIsOpen?: boolean;
/** Default open state for uncontrolled usage */
defaultIsOpen?: boolean;
/** Currently selected item */
selectedItem?: Item | null;
/** Initial selected item when component mounts */
initialSelectedItem?: Item | null;
/** Default selected item for uncontrolled usage */
defaultSelectedItem?: Item | null;
/** Current input value */
inputValue?: string;
/** Initial input value when component mounts */
initialInputValue?: string;
/** Default input value for uncontrolled usage */
defaultInputValue?: string;
/** Custom ID for the component */
id?: string;
/** ID for the label element */
labelId?: string;
/** ID for the menu element */
menuId?: string;
/** ID for the toggle button element */
toggleButtonId?: string;
/** ID for the input element */
inputId?: string;
/** Function to generate IDs for menu items */
getItemId?: (index: number) => string;
/** Custom function to handle scrolling items into view */
scrollIntoView?: (node: HTMLElement, menuNode: HTMLElement) => void;
/** Custom state reducer for advanced state management */
stateReducer?: (
state: UseComboboxState<Item>,
actionAndChanges: UseComboboxStateChangeOptions<Item>
) => Partial<UseComboboxState<Item>>;
/** Callback when selected item changes */
onSelectedItemChange?: (changes: UseComboboxSelectedItemChange<Item>) => void;
/** Callback when open state changes */
onIsOpenChange?: (changes: UseComboboxIsOpenChange<Item>) => void;
/** Callback when highlighted index changes */
onHighlightedIndexChange?: (changes: UseComboboxHighlightedIndexChange<Item>) => void;
/** Callback when input value changes */
onInputValueChange?: (changes: UseComboboxInputValueChange<Item>) => void;
/** Callback when any state changes */
onStateChange?: (changes: UseComboboxStateChange<Item>) => void;
/** Function to generate accessibility status messages */
getA11yStatusMessage?: (options: UseComboboxState<Item>) => string;
/** Environment object for SSR/testing scenarios */
environment?: Environment;
}
interface UseComboboxReturnValue<Item> {
/** Current highlighted item index */
highlightedIndex: number;
/** Currently selected item */
selectedItem: Item | null;
/** Whether the dropdown menu is open */
isOpen: boolean;
/** Current input value */
inputValue: string;
/** Actions for controlling the combobox programmatically */
reset: () => void;
openMenu: () => void;
closeMenu: () => void;
toggleMenu: () => void;
selectItem: (item: Item | null) => void;
setHighlightedIndex: (index: number) => void;
setInputValue: (inputValue: string) => void;
/** Prop getters for UI elements */
getInputProps: <Options>(
options?: UseComboboxGetInputPropsOptions & Options,
otherOptions?: GetPropsCommonOptions
) => UseComboboxGetInputPropsReturnValue;
getToggleButtonProps: <Options>(
options?: UseComboboxGetToggleButtonPropsOptions & Options
) => UseComboboxGetToggleButtonPropsReturnValue;
getLabelProps: <Options>(
options?: UseComboboxGetLabelPropsOptions & Options
) => UseComboboxGetLabelPropsReturnValue;
getMenuProps: <Options>(
options?: UseComboboxGetMenuPropsOptions & Options,
otherOptions?: GetPropsCommonOptions
) => UseComboboxGetMenuPropsReturnValue;
getItemProps: <Options>(
options: UseComboboxGetItemPropsOptions<Item> & Options
) => UseComboboxGetItemPropsReturnValue;
}Usage Examples:
import { useCombobox } from 'downshift';
// Basic autocomplete example
function BasicCombobox() {
const items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];
const [inputItems, setInputItems] = useState(items);
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
} = useCombobox({
items: inputItems,
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(item =>
item.toLowerCase().startsWith(inputValue.toLowerCase())
)
);
},
});
return (
<div>
<label {...getLabelProps()}>Choose a fruit:</label>
<div style={{ display: 'inline-flex' }}>
<input {...getInputProps()} />
<button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
↓
</button>
</div>
<ul {...getMenuProps()}>
{isOpen &&
inputItems.map((item, index) => (
<li
style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
))}
</ul>
</div>
);
}
// Advanced combobox with complex objects
function AdvancedCombobox() {
const allItems = [
{ id: 1, name: 'Apple', category: 'fruit', color: 'red' },
{ id: 2, name: 'Banana', category: 'fruit', color: 'yellow' },
{ id: 3, name: 'Carrot', category: 'vegetable', color: 'orange' },
{ id: 4, name: 'Date', category: 'fruit', color: 'brown' },
];
const [inputItems, setInputItems] = useState(allItems);
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
inputValue,
} = useCombobox({
items: inputItems,
itemToString: (item) => (item ? item.name : ''),
onInputValueChange: ({ inputValue }) => {
setInputItems(
allItems.filter(item =>
!inputValue || item.name.toLowerCase().includes(inputValue.toLowerCase())
)
);
},
onSelectedItemChange: ({ selectedItem }) => {
console.log('Selected:', selectedItem);
// Reset input after selection
setInputItems(allItems);
},
});
return (
<div>
<label {...getLabelProps()}>Search items:</label>
<div style={{ display: 'inline-flex' }}>
<input {...getInputProps()} placeholder="Type to search..." />
<button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
{isOpen ? '↑' : '↓'}
</button>
</div>
<ul {...getMenuProps()}>
{isOpen &&
inputItems.map((item, index) => (
<li
style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
key={item.id}
{...getItemProps({ item, index })}
>
<strong>{item.name}</strong> ({item.category}) - {item.color}
</li>
))}
</ul>
{selectedItem && (
<div>Selected: {selectedItem.name}</div>
)}
</div>
);
}The hook returns current state and action functions for programmatic control.
interface UseComboboxState<Item> {
/** Index of currently highlighted item (-1 if none) */
highlightedIndex: number;
/** Currently selected item */
selectedItem: Item | null;
/** Whether dropdown menu is open */
isOpen: boolean;
/** Current input value */
inputValue: string;
}
interface UseComboboxActions<Item> {
/** Reset to initial state */
reset: () => void;
/** Open the dropdown menu */
openMenu: () => void;
/** Close the dropdown menu */
closeMenu: () => void;
/** Toggle the dropdown menu open/closed */
toggleMenu: () => void;
/** Select a specific item */
selectItem: (item: Item | null) => void;
/** Set the highlighted index */
setHighlightedIndex: (index: number) => void;
/** Set the input value */
setInputValue: (inputValue: string) => void;
}Prop getters return props to spread onto DOM elements with proper event handlers and accessibility attributes.
interface UseComboboxGetInputPropsOptions extends React.HTMLProps<HTMLInputElement> {
/** Custom ref key (defaults to 'ref') */
refKey?: string;
/** Whether the input is disabled */
disabled?: boolean;
}
interface UseComboboxGetInputPropsReturnValue {
'aria-activedescendant': string;
'aria-autocomplete': 'list';
'aria-controls': string;
'aria-expanded': boolean;
'aria-labelledby': string | undefined;
autoComplete: 'off';
id: string;
role: 'combobox';
value: string;
ref?: React.RefObject<any>;
onChange?: React.ChangeEventHandler;
onClick?: React.MouseEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onBlur?: React.FocusEventHandler;
}
interface UseComboboxGetToggleButtonPropsOptions extends React.HTMLProps<HTMLButtonElement> {
/** Custom ref key (defaults to 'ref') */
refKey?: string;
/** Event handler for React Native press events */
onPress?: (event: React.BaseSyntheticEvent) => void;
}
interface UseComboboxGetToggleButtonPropsReturnValue {
'aria-controls': string;
'aria-expanded': boolean;
id: string;
tabIndex: -1;
ref?: React.RefObject<any>;
onClick?: React.MouseEventHandler;
onPress?: (event: React.BaseSyntheticEvent) => void;
}
interface UseComboboxGetMenuPropsReturnValue {
'aria-labelledby': string | undefined;
role: 'listbox';
id: string;
ref?: React.RefObject<any>;
onMouseLeave: React.MouseEventHandler;
}
interface UseComboboxGetItemPropsOptions<Item> {
/** The item this props object is for */
item: Item;
/** Index of the item in the items array */
index?: number;
/** Custom ref key (defaults to 'ref') */
refKey?: string;
}
interface UseComboboxGetItemPropsReturnValue {
'aria-disabled': boolean;
'aria-selected': boolean;
id: string;
role: 'option';
ref?: React.RefObject<any>;
onClick?: React.MouseEventHandler;
onMouseDown?: React.MouseEventHandler;
onMouseMove?: React.MouseEventHandler;
onPress?: React.MouseEventHandler;
}Constants for identifying different types of state changes in the state reducer.
enum UseComboboxStateChangeTypes {
InputKeyDownArrowDown = '__input_keydown_arrow_down__',
InputKeyDownArrowUp = '__input_keydown_arrow_up__',
InputKeyDownEscape = '__input_keydown_escape__',
InputKeyDownHome = '__input_keydown_home__',
InputKeyDownEnd = '__input_keydown_end__',
InputKeyDownPageUp = '__input_keydown_page_up__',
InputKeyDownPageDown = '__input_keydown_page_down__',
InputKeyDownEnter = '__input_keydown_enter__',
InputChange = '__input_change__',
InputBlur = '__input_blur__',
InputClick = '__input_click__',
MenuMouseLeave = '__menu_mouse_leave__',
ItemMouseMove = '__item_mouse_move__',
ItemClick = '__item_click__',
ToggleButtonClick = '__togglebutton_click__',
FunctionToggleMenu = '__function_toggle_menu__',
FunctionOpenMenu = '__function_open_menu__',
FunctionCloseMenu = '__function_close_menu__',
FunctionSetHighlightedIndex = '__function_set_highlighted_index__',
FunctionSelectItem = '__function_select_item__',
FunctionSetInputValue = '__function_set_input_value__',
FunctionReset = '__function_reset__',
ControlledPropUpdatedSelectedItem = '__controlled_prop_updated_selected_item__',
}Access via useCombobox.stateChangeTypes:
import { useCombobox } from 'downshift';
const stateReducer = (state, actionAndChanges) => {
switch (actionAndChanges.type) {
case useCombobox.stateChangeTypes.InputChange:
// Handle input value changes
return {
...actionAndChanges.changes,
// Custom logic here
};
case useCombobox.stateChangeTypes.ItemClick:
// Handle item selection
return {
...actionAndChanges.changes,
isOpen: false, // Always close menu on item click
};
default:
return actionAndChanges.changes;
}
};The combobox can be combined with useMultipleSelection for multi-select autocomplete scenarios:
import { useCombobox, useMultipleSelection } from 'downshift';
function MultiCombobox() {
const items = ['Apple', 'Banana', 'Cherry', 'Date'];
const [inputItems, setInputItems] = useState(items);
const {
getSelectedItemProps,
getDropdownProps,
addSelectedItem,
removeSelectedItem,
selectedItems,
} = useMultipleSelection({
initialSelectedItems: [],
});
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectItem,
} = useCombobox({
items: inputItems,
stateReducer: (state, actionAndChanges) => {
const { changes, type } = actionAndChanges;
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // Keep menu open
highlightedIndex: 0,
inputValue: '',
};
default:
return changes;
}
},
onStateChange: ({ inputValue, type, selectedItem }) => {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
if (selectedItem) {
addSelectedItem(selectedItem);
setInputItems(items.filter(item => !selectedItems.includes(item) && item !== selectedItem));
}
break;
case useCombobox.stateChangeTypes.InputChange:
setInputItems(
items.filter(
item =>
!selectedItems.includes(item) &&
item.toLowerCase().startsWith(inputValue.toLowerCase())
)
);
break;
default:
break;
}
},
onInputValueChange: ({ inputValue }) => {
setInputItems(
items.filter(
item =>
!selectedItems.includes(item) &&
item.toLowerCase().startsWith(inputValue.toLowerCase())
)
);
},
});
return (
<div>
<label {...getLabelProps()}>Choose fruits:</label>
<div {...getDropdownProps()}>
{selectedItems.map((selectedItem, index) => (
<span
key={`selected-item-${index}`}
{...getSelectedItemProps({ selectedItem, index })}
>
{selectedItem}
<span onClick={() => removeSelectedItem(selectedItem)}>×</span>
</span>
))}
<input {...getInputProps()} />
<button type="button" {...getToggleButtonProps()} aria-label="toggle menu">
↓
</button>
</div>
<ul {...getMenuProps()}>
{isOpen &&
inputItems.map((item, index) => (
<li
style={highlightedIndex === index ? { backgroundColor: '#bde4ff' } : {}}
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{item}
</li>
))}
</ul>
</div>
);
}