These five packages help developers build select dropdowns, autocomplete inputs, and searchable comboboxes in React applications. react-select is the most feature-complete solution with built-in styling and async support. downshift provides low-level primitives for building custom accessible components from scratch. react-autosuggest focuses specifically on suggestion boxes with flexible rendering. react-select-search offers a lightweight alternative to react-select with simpler customization. @commercetools-uikit/async-select-input is a specialized component from the Commercetools UI kit designed for their design system with async loading built in.
Select dropdowns and autocomplete inputs are everywhere in modern web applications. But building them well is harder than it looks โ you need to handle keyboard navigation, accessibility, async data loading, and custom styling. The five packages we're comparing (@commercetools-uikit/async-select-input, downshift, react-autosuggest, react-select, react-select-search) each take different approaches to solving these problems. Let's break down how they work and when to use each one.
The biggest difference between these packages is how much they give you out of the box.
react-select is a complete, styled component. You import it and get a working select with minimal code.
import Select from 'react-select';
function MyComponent() {
const options = [
{ value: 'chocolate', label: 'Chocolate' },
{ value: 'strawberry', label: 'Strawberry' }
];
return <Select options={options} />;
}
downshift gives you hooks and utilities to build your own component. You control every part of the UI.
import { useSelect } from 'downshift';
function MySelect({ items }) {
const {
isOpen,
getToggleButtonProps,
getMenuProps,
getItemProps,
selectedItem
} = useSelect({ items });
return (
<div>
<button {...getToggleButtonProps()}>
{selectedItem?.label || 'Select an item'}
</button>
<ul {...getMenuProps()}>
{isOpen && items.map((item, index) => (
<li key={item.id} {...getItemProps({ item, index })}>
{item.label}
</li>
))}
</ul>
</div>
);
}
react-autosuggest focuses on suggestion boxes where users type to get recommendations.
import Autosuggest from 'react-autosuggest';
function MyAutosuggest({ suggestions }) {
const [value, setValue] = useState('');
const [suggestionsState, setSuggestions] = useState([]);
const onSuggestionsFetchRequested = ({ value }) => {
setSuggestions(getSuggestions(value));
};
return (
<Autosuggest
suggestions={suggestionsState}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion => <div>{suggestion.name}</div>}
inputProps={{ value, onChange: (e, { newValue }) => setValue(newValue) }}
/>
);
}
react-select-search sits between react-select and downshift โ pre-built but simpler.
import { ReactSelectSearch } from 'react-select-search';
function MyComponent() {
const options = [
{ name: 'Chocolate', value: 'chocolate' },
{ name: 'Strawberry', value: 'strawberry' }
];
return <ReactSelectSearch options={options} />;
}
@commercetools-uikit/async-select-input is designed for the Commercetools design system with async loading built in.
import AsyncSelectInput from '@commercetools-uikit/async-select-input';
function MyComponent() {
const loadOptions = async (inputValue) => {
const response = await fetch(`/api/options?q=${inputValue}`);
return response.json();
};
return <AsyncSelectInput loadOptions={loadOptions} />;
}
Loading options from an API is a common requirement. Here's how each package handles it.
react-select has built-in async support with AsyncSelect component.
import AsyncSelect from 'react-select/async';
const loadOptions = async (inputValue) => {
const response = await fetch(`/api/options?q=${inputValue}`);
const data = await response.json();
return data.options;
};
function MyComponent() {
return <AsyncSelect loadOptions={loadOptions} />;
}
downshift requires you to manage async state yourself.
import { useCombobox } from 'downshift';
function AsyncCombobox() {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const handleInputChange = async (inputValue) => {
setIsLoading(true);
const response = await fetch(`/api/options?q=${inputValue}`);
const data = await response.json();
setItems(data.options);
setIsLoading(false);
};
const { getInputProps, getMenuProps, getItemProps } = useCombobox({
items,
onInputValueChange: ({ inputValue }) => handleInputChange(inputValue)
});
return (
<div>
<input {...getInputProps()} />
<ul {...getMenuProps()}>
{items.map((item, index) => (
<li key={item.id} {...getItemProps({ item, index })}>
{item.label}
</li>
))}
</ul>
</div>
);
}
react-autosuggest uses callback-based fetching.
import Autosuggest from 'react-autosuggest';
const onSuggestionsFetchRequested = async ({ value }) => {
const response = await fetch(`/api/options?q=${value}`);
const data = await response.json();
setSuggestions(data.options);
};
function MyComponent() {
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
getSuggestionValue={suggestion => suggestion.label}
renderSuggestion={suggestion => <div>{suggestion.label}</div>}
inputProps={{ value, onChange }}
/>
);
}
react-select-search supports async through custom props.
import { ReactSelectSearch } from 'react-select-search';
function MyComponent() {
const [options, setOptions] = useState([]);
const handleSearch = async (searchTerm) => {
const response = await fetch(`/api/options?q=${searchTerm}`);
const data = await response.json();
setOptions(data.options);
};
return <ReactSelectSearch options={options} onSearch={handleSearch} />;
}
@commercetools-uikit/async-select-input has async loading as its primary feature.
import AsyncSelectInput from '@commercetools-uikit/async-select-input';
function MyComponent() {
const loadOptions = async (inputValue) => {
const response = await fetch(`/api/options?q=${inputValue}`);
return response.json();
};
return (
<AsyncSelectInput
loadOptions={loadOptions}
loadingPlaceholder="Loading..."
/>
);
}
Accessibility is critical for select components. Keyboard navigation, ARIA attributes, and screen reader support all matter.
react-select handles accessibility out of the box with proper ARIA labels and keyboard support.
import Select from 'react-select';
// Built-in accessibility - no extra code needed
<Select
options={options}
aria-label="Choose a flavor"
inputId="flavor-select"
/>
downshift provides the tools but you must apply them correctly.
import { useSelect } from 'downshift';
function AccessibleSelect({ items, label }) {
const {
isOpen,
getToggleButtonProps,
getMenuProps,
getItemProps,
selectedItem,
highlightedIndex
} = useSelect({ items });
return (
<div>
<label id="select-label">{label}</label>
<button
{...getToggleButtonProps({
'aria-labelledby': 'select-label',
'aria-haspopup': 'listbox'
})}
>
{selectedItem?.label || 'Select'}
</button>
<ul {...getMenuProps({ 'aria-labelledby': 'select-label' })}>
{isOpen && items.map((item, index) => (
<li
key={item.id}
{...getItemProps({
item,
index,
role: 'option',
'aria-selected': highlightedIndex === index
})}
>
{item.label}
</li>
))}
</ul>
</div>
);
}
react-autosuggest includes accessibility features but requires proper configuration.
import Autosuggest from 'react-autosuggest';
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion => <div>{suggestion.name}</div>}
inputProps={{
value,
onChange,
'aria-label': 'Search for options',
id: 'autosuggest-input',
role: 'combobox',
'aria-autocomplete': 'list',
'aria-controls': 'suggestions-list',
'aria-expanded': suggestions.length > 0
}}
theme={{
suggestionsContainerOpen: 'suggestions-container-open',
suggestion: 'suggestion',
suggestionHighlighted: 'suggestion-highlighted'
}}
/>
react-select-search provides basic accessibility with proper ARIA attributes.
import { ReactSelectSearch } from 'react-select-search';
<ReactSelectSearch
options={options}
ariaLabel="Select an option"
inputId="select-search-input"
className="custom-select-search"
/>
@commercetools-uikit/async-select-input follows Commercetools accessibility standards.
import AsyncSelectInput from '@commercetools-uikit/async-select-input';
<AsyncSelectInput
loadOptions={loadOptions}
label="Select option"
name="option-select"
aria-label="Choose an option from the list"
/>
How you customize the look varies significantly between packages.
react-select uses CSS-in-JS with a styles prop for deep customization.
import Select from 'react-select';
const customStyles = {
control: (base, state) => ({
...base,
borderColor: state.isFocused ? '#0066cc' : base.borderColor,
boxShadow: state.isFocused ? '0 0 0 1px #0066cc' : base.boxShadow
}),
option: (base, state) => ({
...base,
backgroundColor: state.isSelected ? '#0066cc' : base.backgroundColor,
color: state.isSelected ? 'white' : base.color
})
};
<Select options={options} styles={customStyles} />;
downshift gives you plain HTML elements โ style them however you want.
import { useSelect } from 'downshift';
function StyledSelect({ items }) {
const { isOpen, getToggleButtonProps, getMenuProps, getItemProps, selectedItem } = useSelect({ items });
return (
<div className="select-container">
<button {...getToggleButtonProps()} className="select-button">
{selectedItem?.label || 'Select'}
</button>
{isOpen && (
<ul {...getMenuProps()} className="select-menu">
{items.map((item, index) => (
<li key={item.id} {...getItemProps({ item, index })} className="select-option">
{item.label}
</li>
))}
</ul>
)}
</div>
);
}
react-autosuggest uses a theme prop with class names.
import Autosuggest from 'react-autosuggest';
const theme = {
container: 'autosuggest-container',
input: 'autosuggest-input',
suggestionsContainer: 'autosuggest-suggestions-container',
suggestionsContainerOpen: 'autosuggest-suggestions-container-open',
suggestion: 'autosuggest-suggestion',
suggestionHighlighted: 'autosuggest-suggestion-highlighted'
};
<Autosuggest suggestions={suggestions} theme={theme} {...otherProps} />;
react-select-search uses className props for customization.
import { ReactSelectSearch } from 'react-select-search';
<ReactSelectSearch
options={options}
className="custom-select-search"
optionClassName="custom-option"
valueClassName="custom-value"
/>
@commercetools-uikit/async-select-input uses Commercetools design tokens.
import AsyncSelectInput from '@commercetools-uikit/async-select-input';
<AsyncSelectInput
loadOptions={loadOptions}
className="custom-async-select"
horizontalConstraint="scale"
/>
Some use cases require selecting multiple values. Here's how each package handles it.
react-select has built-in multi-select with the isMulti prop.
import Select from 'react-select';
<Select
options={options}
isMulti={true}
placeholder="Select multiple flavors"
onChange={(selected) => console.log(selected)}
/>
downshift requires manual implementation for multi-select.
import { useMultipleSelection } from 'downshift';
function MultiSelect({ items }) {
const {
selectedItems,
addSelectedItem,
removeSelectedItem,
getDropdownProps
} = useMultipleSelection();
const {
isOpen,
getToggleButtonProps,
getMenuProps,
getItemProps
} = useSelect({
items,
...getDropdownProps()
});
return (
<div>
{selectedItems.map(item => (
<span key={item.id}>
{item.label}
<button onClick={() => removeSelectedItem(item)}>ร</button>
</span>
))}
<button {...getToggleButtonProps()}>Select</button>
<ul {...getMenuProps()}>
{isOpen && items.map((item, index) => (
<li
key={item.id}
{...getItemProps({
item,
index,
onClick: () => addSelectedItem(item)
})}
>
{item.label}
</li>
))}
</ul>
</div>
);
}
react-autosuggest doesn't have built-in multi-select โ you manage selected values yourself.
import Autosuggest from 'react-autosuggest';
function MultiAutosuggest({ suggestions }) {
const [selected, setSelected] = useState([]);
const [value, setValue] = useState('');
const onSuggestionSelected = (event, { suggestion }) => {
setSelected([...selected, suggestion]);
setValue('');
};
return (
<div>
{selected.map(item => <span key={item.id}>{item.name}</span>)}
<Autosuggest
suggestions={suggestions}
onSuggestionSelected={onSuggestionSelected}
inputProps={{ value, onChange }}
/>
</div>
);
}
react-select-search supports multi-select through configuration.
import { ReactSelectSearch } from 'react-select-search';
<ReactSelectSearch
options={options}
multi={true}
onChange={(selected) => console.log(selected)}
/>
@commercetools-uikit/async-select-input focuses on single-select async scenarios.
import AsyncSelectInput from '@commercetools-uikit/async-select-input';
// Single select only - multi-select requires different component
<AsyncSelectInput
loadOptions={loadOptions}
value={selectedValue}
onChange={setSelectedValue}
/>
Before choosing any package, check its current maintenance status.
react-select is actively maintained with regular releases. It's the most popular choice for production select components and has strong community support.
downshift is actively maintained by the Kent C. Dodds team and widely used in design systems. It's stable and follows accessibility best practices.
react-autosuggest is mature and stable but receives fewer updates. It's well-suited for projects that need a reliable autocomplete without frequent changes.
react-select-search has slower release cycles but remains functional. Check the repository for recent activity before committing to it for large projects.
@commercetools-uikit/async-select-input is maintained as part of the Commercetools UI Kit. It's best suited for projects already using Commercetools products โ using it standalone may create dependency issues long-term.
| Feature | react-select | downshift | react-autosuggest | react-select-search | @commercetools-uikit |
|---|---|---|---|---|---|
| Ready-Made UI | โ Full | โ Headless | โ Focused | โ Simple | โ Design System |
| Async Loading | โ Built-in | โ ๏ธ Manual | โ Callback | โ Custom | โ Built-in |
| Multi-Select | โ Built-in | โ ๏ธ Manual | โ ๏ธ Manual | โ Config | โ Single Only |
| Accessibility | โ Automatic | โ ๏ธ Your Responsibility | โ Configured | โ Basic | โ Standards |
| Styling | CSS-in-JS | Any CSS | Class Names | Class Names | Design Tokens |
| Bundle Size | Larger | Smaller | Medium | Smaller | Medium |
| Learning Curve | Low | High | Medium | Low | Low (if using UI Kit) |
Pick react-select when you need a production-ready component fast and don't mind the bundle size. It's the safest choice for most admin panels and dashboards.
Pick downshift when you're building a design system or need complete control over the UI. The extra code is worth it for custom designs.
Pick react-autosuggest when you specifically need a search-as-you-type suggestion box rather than a full select dropdown.
Pick react-select-search when react-select feels too heavy but you still need search functionality in a select.
Pick @commercetools-uikit/async-select-input only if you're already using the Commercetools UI Kit โ otherwise, the tight coupling to their design system creates unnecessary constraints.
All five packages solve the same core problem but with different trade-offs. react-select and react-select-search give you working components quickly. downshift gives you building blocks for custom solutions. react-autosuggest specializes in suggestion boxes. @commercetools-uikit/async-select-input fits into a specific ecosystem.
The right choice depends on your project's needs: speed to market, design flexibility, bundle size constraints, and existing technology stack. There's no single winner โ only the best fit for your specific situation.
Choose this package if you're already using the Commercetools UI Kit and need a select component that matches their design system. It's purpose-built for async loading scenarios and integrates seamlessly with other Commercetools components. However, it's tightly coupled to their design tokens and may require significant customization if used outside their ecosystem. Best for projects already committed to the Commercetools platform.
Choose downshift if you need full control over the UI and want to build a custom select or autocomplete from scratch. It handles accessibility, keyboard navigation, and state management while leaving styling completely up to you. Ideal for design systems or when existing libraries don't match your visual requirements. Expect to write more code but gain maximum flexibility.
Choose react-autosuggest if you need a focused suggestion box component with flexible rendering and theming. It's well-suited for search inputs where users type to get recommendations. The library is mature and stable but less feature-rich than react-select for complex select scenarios. Best for autocomplete-specific use cases rather than full select dropdowns.
Choose react-select if you need a production-ready, feature-complete select component with minimal setup. It includes async loading, multi-select, grouping, and extensive customization options out of the box. The trade-off is larger bundle size and more complex styling overrides. Ideal for admin panels, dashboards, and applications where development speed matters more than bundle size.
Choose react-select-search if you want something lighter than react-select but still need search functionality within a select dropdown. It offers simpler customization and a smaller footprint while maintaining core select features. Good for projects that find react-select too heavy but need more than a basic native select element.
An input component getting a selection from an asynchronously loaded list from the user.
yarn add @commercetools-uikit/async-select-input
npm --save install @commercetools-uikit/async-select-input
Additionally install the peer dependencies (if not present)
yarn add react react-dom react-intl
npm --save install react react-dom react-intl
import AsyncSelectInput from '@commercetools-uikit/async-select-input';
const Example = (props) => (
<AsyncSelectInput
value={{ value: 'ready', label: 'Ready' }}
loadOptions={
(/* inputValue */) => {
// async fetch logic
}
}
onChange={(event) => alert(event.target.value)}
/>
);
export default Example;
| Props | Type | Required | Default | Description |
|---|---|---|---|---|
horizontalConstraint | unionPossible values: , 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 'scale', 'auto' | Horizontal size limit of the input fields. | ||
hasError | boolean | Indicates the input field has an error | ||
hasWarning | boolean | Indicates the input field has a warning | ||
isReadOnly | boolean | Indicates that the field is displaying read-only content | ||
aria-label | AsyncProps['aria-label'] | Aria label (for assistive tech)
Props from React select was used | ||
aria-labelledby | AsyncProps['aria-labelledby'] | HTML ID of an element that should be used as the label (for assistive tech)
Props from React select was used | ||
aria-invalid | AsyncProps['aria-invalid'] | Indicate if the value entered in the input is invalid.
Props from React select was used | ||
aria-errormessage | AsyncProps['aria-errormessage'] | HTML ID of an element containing an error message related to the input.
Props from React select was used | ||
isAutofocussed | boolean | Focus the control when it is mounted | ||
backspaceRemovesValue | AsyncProps['backspaceRemovesValue'] | Remove the currently focused option when the user presses backspace
Props from React select was used | ||
components | AsyncProps['components'] | Map of components to overwrite the default ones, see what components you can override
Props from React select was used | ||
controlShouldRenderValue | AsyncProps['controlShouldRenderValue'] | true | Control whether the selected values should be rendered in the control
Props from React select was used | |
filterOption | AsyncProps['filterOption'] | Custom method to filter whether an option should be displayed in the menu
Props from React select was used | ||
hideSelectedOptions | AsyncProps['hideSelectedOptions'] | Custom method to determine whether selected options should be displayed in the menu
Props from React select was used | ||
id | AsyncProps['inputId'] | The id of the search input
Props from React select was used | ||
inputValue | AsyncProps['inputValue'] | The value of the search input
Props from React select was used | ||
containerId | AsyncProps['id'] | The id to set on the SelectContainer component
Props from React select was used | ||
isClearable | AsyncProps['isClearable'] | Is the select value clearable
Props from React select was used | ||
isCondensed | boolean | Use this property to reduce the paddings of the component for a ui compact variant | ||
isDisabled | AsyncProps['isDisabled'] | Is the select disabled
Props from React select was used | ||
isOptionDisabled | AsyncProps['isOptionDisabled'] | Override the built-in logic to detect whether an option is disabled
Props from React select was used | ||
isMulti | AsyncProps['isMulti'] | Support multiple selected options
Props from React select was used | ||
isSearchable | AsyncProps['isSearchable'] | true | Whether to enable search functionality
Props from React select was used | |
menuIsOpen | AsyncProps['menuIsOpen'] | Can be used to enforce the select input to be opened
Props from React select was used | ||
maxMenuHeight | AsyncProps['maxMenuHeight'] | Maximum height of the menu before scrolling
Props from React select was used | ||
menuPortalTarget | AsyncProps['menuPortalTarget'] | Dom element to portal the select menu to
Props from React select was used | ||
menuPortalZIndex | number | 1 | z-index value for the menu portal
Use in conjunction with menuPortalTarget | |
menuShouldBlockScroll | AsyncProps['menuShouldBlockScroll'] | whether the menu should block scroll while open
Props from React select was used | ||
closeMenuOnSelect | AsyncProps['closeMenuOnSelect'] | Whether the menu should close after a value is selected. Defaults to true.
Props from React select was used | ||
name | AsyncProps['name'] | Name of the HTML Input (optional - without this, no input will be rendered)
Props from React select was used | ||
noOptionsMessage | AsyncProps['noOptionsMessage'] | Can be used to render a custom value when there are no options (either because of no search results, or all options have been used, or there were none in the first place). Gets called with { inputValue: String }. inputValue will be an empty string when no search text is present.
Props from React select was used | ||
onBlur | FunctionSee signature. | Handle blur events on the control | ||
onChange | FunctionSee signature. | Called with a fake event when value changes. The event's target.name will be the name supplied in props. The event's target.value will hold the value. The value will be the selected option, or an array of options in case isMulti is true. | ||
onFocus | AsyncProps['onFocus'] | Handle focus events on the control
Props from React select was used | ||
onInputChange | AsyncProps['onInputChange'] | Handle change events on the input
Props from React select was used | ||
placeholder | AsyncProps['placeholder'] | Placeholder text for the select value
Props from React select was used | ||
loadingMessage | unionPossible values: string , (() => string) | loading message shown while the options are being loaded | ||
tabIndex | AsyncProps['tabIndex'] | Sets the tabIndex attribute on the input
Props from React select was used | ||
tabSelectsValue | AsyncProps['tabSelectsValue'] | Select the currently focused option when the user presses tab
Props from React select was used | ||
value | AsyncProps['value'] | null | The value of the select; reflected by the selected option
Props from React select was used | |
defaultOptions | unionPossible values: OptionsOrGroups<unknown, GroupBase<unknown>> , boolean | The default set of options to show before the user starts searching. When set to true, the results for loadOptions('') will be autoloaded.
Props from React select was used | ||
loadOptions | AsyncProps['loadOptions'] | โ | Function that returns a promise, which is the set of options to be used once the promise resolves. | |
cacheOptions | AsyncProps['cacheOptions'] | If cacheOptions is truthy, then the loaded data will be cached. The cache will remain until cacheOptions changes value. | ||
showOptionGroupDivider | boolean | Determines if option groups will be separated by a divider | ||
iconLeft | ReactNode | Icon to display on the left of the placeholder text and selected value. Has no effect when isMulti is enabled. | ||
optionStyle | unionPossible values: 'list' , 'checkbox' | 'list' | defines how options are rendered | |
appearance | unionPossible values: 'default' , 'filter' | 'default' | Indicates the appearance of the input. Filter appearance is meant to be used when the async-select is used as a filter. | |
count | number | An additional value displayed on the select options menu. This value is only available in the checkbox option style when appearance is set to filter. |
onBlur(event: TCustomEvent) => void
onChange(event: TCustomEvent, info: ActionMeta<unknown>) => void
This input is built on top of react-select v2.
It supports mostly same properties as react-select. Behavior for some props was changed, and support for others was dropped.
In case you need one of the currently excluded props, feel free to open a PR adding them.
isTouched(touched)Returns truthy value for the Formik touched value of this input field.
It is possible to customize AsyncSelectInput by passing the components property.
AsyncSelectInput exports the default underlying components as static exports.
Components available as static exports are:
ClearIndicatorControlCrossIconDownChevronDropdownIndicatorGroupGroupHeadingIndicatorsContainerIndicatorSeparatorInputLoadingIndicatorLoadingMessageMenuMenuListMenuPortalMultiValueMultiValueContainerMultiValueLabelMultiValueRemoveNoOptionsMessageOptionPlaceholderSelectContainerSingleValueValueContainerSee the official documentation for more information about the props they receive.