Combobox
Combobox is a composable component which allows users to filter a list of items. It includes an input field that acts as a trigger, a popover which display the list of items.
Quick Start
- Installation
npm install @adaptavant/eds-core
- Import
import { Combobox } from '@adaptavant/eds-core';
Open Menu
By default, the menu opens when the user types in the trigger input. Also, using menuTrigger="focus"
prop allows you to open the menu when the input is focused.
const initialOptions = [
{ id: '1', value: 'Item 1' },
{ id: '2', value: 'Item 2' },
{ id: '3', value: 'Item 3' },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const filteredOptions = React.useMemo(() => {
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Field label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
menuTrigger="focus"
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
options={filteredOptions}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxSearchInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
);
Composing Trigger
Trigger as Search Input
const initialOptions = [
{ id: '1', value: 'Item 1' },
{ id: '2', value: 'Item 2' },
{ id: '3', value: 'Item 3' },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const filteredOptions = React.useMemo(() => {
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Field label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
options={filteredOptions}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxSearchInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
);
Trigger as Text Input
const initialOptions = [
{ id: '1', value: 'Item 1' },
{ id: '2', value: 'Item 2' },
{ id: '3', value: 'Item 3' },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const filteredOptions = React.useMemo(() => {
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Field label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
options={filteredOptions}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxTextInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
);
Also, if you wanted to have a custom clear.
const initialOptions = [
{ id: '1', value: 'Item 1' },
{ id: '2', value: 'Item 2' },
{ id: '3', value: 'Item 3' },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const filteredOptions = React.useMemo(() => {
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Field label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
options={filteredOptions}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxTextInput
adornmentEnd={
searchTerm && (
<ClickableAdornment
className="text-primary leading-none"
onClick={() => {
setSearchTerm('');
setSelectedOption(undefined);
}}
>
<RemoveIcon size="16" />
</ClickableAdornment>
)
}
classNames={{
adornmentEnd: 'pe-2',
}}
placeholder="Search..."
/>
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
);
Popover Customizations
The Combobox component provides several props to customise the appearance and behavior of its popover. These props allow for fine-grained control over the popover’s width, height, offset, and placement relative to the trigger element.
popoverMatchReferenceWidth
: Set totrue
to make the popover’s width match the trigger’s width, orfalse
for independent width.popoverMaxHeight
: Sets the maximum height of the popover in pixels. The default is356
.popoverMaxWidth
: Sets the maximum width of the popover in pixels. The default is400
.popoverOffset
: Adjusts the space between the popover and the trigger, specified in pixels. The default is4
.popoverPlacement
: Determines the position of the popover relative to the trigger. Options include 'bottom', 'bottom-start', and 'bottom-end', allowing for flexible positioning based on the layout and space available. The default is 'bottom-start'. If there isn’t enough space for the popover to appear below the trigger, it will automatically switch to the top position.
For example:
<Combobox
popoverMatchReferenceWidth={}
popoverMaxHeight={}
popoverMaxWidth={}
popoverOffset={}
popoverPlacement={}
{...props}
>
...
</Combobox>
Strategy
Use the strategy
prop to change the way how popover element will be positioned. By default, the strategy is set to absolute
, changes it to fixed
when the ComboboxTrigger is inside a sticky or fixed element.
This option leverages the floating-ui library, which powers the ComboboxPopover functionality.
const initialOptions = [
{ id: '1', value: 'Item 1' },
{ id: '2', value: 'Item 2' },
{ id: '3', value: 'Item 3' },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const [showFixedElement, setShowFixedElement] = React.useState(false);
const onButtonClick = () => {
setShowFixedElement((prevState) => !prevState)
}
const filteredOptions = React.useMemo(() => {
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Stack className="w-full gap-4">
{showFixedElement ? (
<div className="
animate-[snackbar-transition_0.3s_cubic-bezier(0.16,_1,_0.3,_1)]
bg-neutral-secondary
fixed
flex
items-center
justify-between
mx-2
p-4
right-0
rounded-8px
shadow-40
sm:right-8
sm:w-[360px]
top-8
w-[calc(100%-16px)]
z-10
">
<Field label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
menuTrigger="focus"
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
options={filteredOptions}
selectedKey="value"
selectedOption={selectedOption}
strategy="fixed"
>
<ComboboxSearchInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
<button
className="
focus-visible:focus-ring
font-stronger
px-1
py-0.5
rounded-4px
text-body-12
text-primary
underline
underline-offset-2
"
onClick={onButtonClick}
>
Close
</button>
</div>
) : null}
<Button onClick={onButtonClick}>
Show fixed element
</Button>
</Stack>
);
Disabled
Utilize the isDisabled
prop in the <Field/>
to show that a entire Combobox isn't usable.
const initialOptions = [
{ id: '1', value: 'Item 1' },
{ id: '2', value: 'Item 2' },
{ id: '3', value: 'Item 3' },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const filteredOptions = React.useMemo(() => {
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Field isDisabled label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
menuTrigger="focus"
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
options={filteredOptions}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxSearchInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
);
Disabled MenuItem
Utilize the isDisabled
prop in the <ComboboxItem />
component to indicate that a specific item is not selectable.
This enhances user experience by clearly displaying the disabled state. Additionally, for improved accessibility, keyboard navigation will bypass disabled MenuItems in the Listbox.
const initialOptions = [
{ id: '1', value: 'Item 1', disabled: true },
{ id: '2', value: 'Item 2', disabled: false },
{ id: '3', value: 'Item 3', disabled: true },
{ id: '4', value: 'Item 4', disabled: false },
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState('');
const filteredOptions = React.useMemo(() => {
console.log('searchTerm', searchTerm);
if (searchTerm === '') return initialOptions;
return initialOptions.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
}, [initialOptions, searchTerm]);
const clearAndSelect = (selectedOption) => {
console.log('selectedOption', selectedOption);
setSelectedOption(selectedOption);
setSearchTerm('');
};
return (
<Field label="Select Items">
<Combobox
inputValue={searchTerm || selectedOption?.value}
menuTrigger="focus"
onClear={() => setSearchTerm('')}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxSearchInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListbox
noResultsFallback={
<Text className="text-secondary text-center text-body-12 py-4">
No matching results
</Text>
}
options={filteredOptions}
>
{(item) => <ComboboxItem option={item} isDisabled={item.disabled}>{item.value}</ComboboxItem>}
</ComboboxListbox>
</ComboboxPopover>
</Combobox>
</Field>
);
Note: Disabled item can already be selected option but having any interactions on it won't be possible.
ComboBox Group
The Combobox component supports displaying options in groups, which is useful for organizing related items into categories. This example demonstrates how to use the ComboboxListBoxGroup
and ComboboxListGroup
components to create a groups in combobox.
const initialOptions = [
{
label: "Leaf",
options: [
{ id: "1", value: "Cabbage" },
{ id: "2", value: "Spinach" },
{ id: "3", value: "Wheat grass" },
],
},
{
label: "Beans",
options: [
{ id: "6", value: "Chickpea" },
{ id: "7", value: "Green bean" },
{ id: "8", value: "Horse gram" },
],
},
{
label: "Bulb",
options: [
{ id: "9", value: "Garlic" },
{ id: "10", value: "Onion" },
{ id: "11", value: "Nopal" },
{ id: "12", value: "Ginger" },
{ id: "13", value: "Ginseng" },
],
},
];
const [selectedOption, setSelectedOption] = React.useState();
const [searchTerm, setSearchTerm] = React.useState("");
const filteredOptions = React.useMemo(() => {
if (searchTerm === "") return initialOptions;
return initialOptions.reduce((filtered, { label, options }) => {
const matchingOptions = options.filter((item) =>
item.value.toLowerCase().includes(searchTerm.toLowerCase())
);
if (matchingOptions.length > 0) {
filtered.push({ label, options: matchingOptions });
}
return filtered;
}, []);
}, [searchTerm]);
const clearAndSelect = (selectedOption) => {
setSelectedOption(selectedOption);
setSearchTerm("");
};
return (
<Field label="Select vegetable">
<Combobox
inputValue={searchTerm || selectedOption?.value}
menuTrigger="focus"
onClear={() => setSearchTerm("")}
onInputChange={setSearchTerm}
onSelectionChange={clearAndSelect}
selectedKey="value"
selectedOption={selectedOption}
>
<ComboboxSearchInput placeholder="Search..." />
<ComboboxPopover>
<ComboboxListBoxGroup
options={filteredOptions}
noResultsFallback={
<Text className="text-body-12">No results</Text>
}
>
{({ options, label }) => {
return (
<ComboboxListGroup label={label} options={options}>
{(item) => (
<ComboboxItem
option={item}
onClick={() => {
setSelectedOption(item);
}}
>
{item.value}
</ComboboxItem>
)}
</ComboboxListGroup>
);
}}
</ComboboxListBoxGroup>
</ComboboxPopover>
</Combobox>
</Field>
);
Style API
Our design system components include style props that allow you to easily customize different parts of each component to match your design needs.
Please refer to the Style API documentation for more insights.