Combobox
A powerful dropdown for providing suggestions, filtering out large datasets, and creating new values quickly and efficiently with no overhead.
Loading...
Overview
Resources
Install
yarn add @camp/comboboxUpgrading to Next Gen
🛠️ AI object passing: When using the new ai prop, you will need to pass an object instead of an array of strings.
🔆 New themed prop: Allows users to enable light and dark theming for the Combobox component. Currently, Combobox is only available in light mode (themed=false), and dark mode support is coming soon.
More about theming here.
Migration steps
- Replace imports: Replace the old Combobox imports with the Combobox from
@camp/combobox. - Verify no theme errors: If you see errors, see this page for instructions on how to enable theming
Variations
A combobox is a combination of input and dropdown components.
The dropdown enables a user to select an option from the dataset presented inside a popup that contains a listbox the dataset. The input is used for entering a value, which can either filter out options inside a limited dataset or create a new entry in the dataset.
Filterable Controlled State
The default example at the top of the page is of an uncontrolled state. See below for a controlled state example.
import { Combobox } from '@camp/combobox';
import type { ICharacter } from './mock-data';
import { smash_characters } from './mock-data';
export const FilterableControlled = () => {
const [options, setOptions] = React.useState(smash_characters);
const strings = options.map(({ name }) => name);
const [value, setValue] = React.useState('');
function filterOptions(inputValue?: string) {
const lowerCasedInputValue = inputValue?.toLowerCase();
return function booksFilter(option: ICharacter) {
return (
!inputValue ||
(lowerCasedInputValue && option?.name?.toLowerCase().includes(lowerCasedInputValue)) ||
(lowerCasedInputValue && option?.franchise?.toLowerCase().includes(lowerCasedInputValue))
);
};
}
return (
<Combobox
inputAriaLabel="Choose your character"
options={strings}
inputValue={value}
variant="filterable"
optionToString={({ value }) => value as string}
onNoResults={() => setValue('')}
onSelect={(option) => option && setValue(option)}
onInputValueChange={({ inputValue }) => {
setValue(inputValue as string);
setOptions(smash_characters.filter(filterOptions(inputValue)));
}}
/>
);
};Options as objects
import { Combobox } from '@camp/combobox';
import type { ICharacter } from './mock-data';
import { smash_characters } from './mock-data';
export const Objects = () => {
const [options, setOptions] = React.useState(smash_characters);
function filterOptions(inputValue?: string) {
const lowerCasedInputValue = inputValue?.toLowerCase();
return function booksFilter(option: ICharacter) {
return (
!inputValue ||
(lowerCasedInputValue && option?.name?.toLowerCase().includes(lowerCasedInputValue)) ||
(lowerCasedInputValue && option?.franchise?.toLowerCase().includes(lowerCasedInputValue))
);
};
}
return (
<Combobox
inputAriaLabel="Choose your character"
options={options}
variant="filterable"
renderOption={({ value }) => value?.name}
optionToString={(option) => option?.value?.name as string}
onInputValueChange={({ inputValue }) =>
setOptions(smash_characters.filter(filterOptions(inputValue)))
}
/>
);
};Editable Controlled State
import { Combobox } from '@camp/combobox';
import type { ICharacter } from './mock-data';
import { smash_characters } from './mock-data';
export const EditableControlled = () => {
const [options, setOptions] = React.useState(smash_characters);
const strings = options.map(({ name }) => name);
const [value, setValue] = React.useState('');
function filterOptions(inputValue?: string) {
const lowerCasedInputValue = inputValue?.toLowerCase();
return function booksFilter(option: ICharacter) {
return (
!inputValue ||
(lowerCasedInputValue && option?.name?.toLowerCase().includes(lowerCasedInputValue)) ||
(lowerCasedInputValue && option?.franchise?.toLowerCase().includes(lowerCasedInputValue))
);
};
}
return (
<Combobox
inputAriaLabel="Choose your character"
inputValue={value}
options={strings}
variant="editable"
optionToString={(option) => option?.value as string}
onCreate={(value) => console.log('onCreate fired:', value)}
onSelect={(option) => option && setValue(option)}
onInputValueChange={({ inputValue }) => {
setValue(inputValue as string);
setOptions(smash_characters.filter(filterOptions(inputValue)));
}}
/>
);
};Editable Uncontrolled State
import { Combobox } from '@camp/combobox';
import type { ICharacter } from './mock-data';
import { smash_characters } from './mock-data';
export const EditableUncontrolled = () => {
const [options, setOptions] = React.useState(smash_characters);
const strings = options.map(({ name }) => name);
function filterOptions(inputValue?: string) {
const lowerCasedInputValue = inputValue?.toLowerCase();
return function booksFilter(option: ICharacter) {
return (
!inputValue ||
(lowerCasedInputValue && option?.name?.toLowerCase().includes(lowerCasedInputValue)) ||
(lowerCasedInputValue && option?.franchise?.toLowerCase().includes(lowerCasedInputValue))
);
};
}
return (
<Combobox
inputAriaLabel="Choose your character"
options={strings}
variant="editable"
optionToString={(option) => option?.value as string}
onCreate={(value) => console.log('onCreate fired:', value)}
onInputValueChange={({ inputValue }) =>
setOptions(smash_characters.filter(filterOptions(inputValue)))
}
/>
);
};Editable Multiselect
import { Combobox } from '@camp/combobox';
import type { ICharacter } from './mock-data';
import { smash_characters } from './mock-data';
export const EditableMultiselect = () => {
const [options, setOptions] = React.useState(smash_characters);
const aiOptions = options.map(({ name }, index) => ({ name, ai: index % 2 === 0 }));
const [value, setValue] = React.useState('');
const [selected, setSelected] = React.useState<string[]>([]);
const deselectOption = (optionToRemove) => {
const newSelected = selected.filter((option) => option !== optionToRemove);
setSelected(newSelected);
};
function filterOptions(inputValue?: string) {
const lowerCasedInputValue = inputValue?.toLowerCase();
return function booksFilter(option: ICharacter) {
return (
!inputValue ||
(lowerCasedInputValue && option?.name?.toLowerCase().includes(lowerCasedInputValue)) ||
(lowerCasedInputValue && option?.franchise?.toLowerCase().includes(lowerCasedInputValue))
);
};
}
return (
<Combobox
inputAriaLabel="Choose your character"
inputValue={value}
options={aiOptions}
chipColor="midnight"
chipSize="large"
variant="editable"
optionToString={({ value }) => value?.name as string}
onCreate={(value) => console.log('onCreate fired:', value)}
// this function currently allows you to select duplicates but can add validation here to avoid that
onSelect={(option) => {
if (option) {
// reset input to empty
setValue('');
// reset options in menu to all options
setOptions(smash_characters);
// check if option is already selected
const isAlreadySelected = selected.filter((optSelected) => optSelected === option);
// add selected option to array
const newSelected = isAlreadySelected.length > 0 ? [...selected] : [...selected, option];
setSelected(newSelected);
}
}}
onInputValueChange={({ inputValue }) => {
setValue(inputValue as string);
setOptions(smash_characters.filter(filterOptions(inputValue)));
}}
multiselect
selectedOptions={selected}
deselectOption={deselectOption}
label="Tag to be added"
placeholder="Enter tag"
/>
);
};Custom rendered options

const customRenderOption = ({ value }) => {
return (
<div data-testid={`custom-option-${value?.name}`}>
<div style={{ fontSize: '0.75rem' }}>
<span
style={{
borderBottom: '2px solid darkgray',
}}
>
{value?.name}
</span>
</div>
<small style={{ fontSize: '0.7rem', color: 'gray' }}>{value?.franchise}</small>
</div>
);
};
const ComboboxWithCustomOptions = (props) => {
const { description, optionToString, options, variant, ...rest } = props;
const [currentOptions, setCurrentOptions] = React.useState(options);
const [allOptions, setAllOptions] = React.useState(options);
const handleCreate = (value) => {
console.log('onCreate was called');
if (!value) return;
// create new array with added option
const newAllOptions = allOptions;
newAllOptions.push(value);
// update all options to include new option
setAllOptions(newAllOptions);
// update currentOptions to include new option
setCurrentOptions([value]);
};
return (
<>
<Combobox
inputAriaLabel="Choose a character"
optionToString={optionToString}
onInputValueChange={({ inputValue }) => {
if (typeof options[0] === 'string') {
setCurrentOptions(
options.filter((opt) =>
opt.toLowerCase().includes(inputValue?.trim()?.toLowerCase()),
),
);
} else {
setCurrentOptions(options.filter(filterObjectOptions(inputValue)));
}
}}
options={currentOptions}
variant={variant}
onCreate={handleCreate}
renderOption={customRenderOption}
{...rest}
/>
<Description>{description}</Description>
</>
);
};Multiselect with AiChip
AiChip is integrated with the Combobox component for features that use a combination of user-generated and AI-generated options, to distinguish between the two. In order for options to be display as AiChips once selected, the options must have an ai property set to true. This example also demonstrates the chipSize prop, which can be used to set the size of the AiChip to small, medium, or large.

import React, { useState } from 'react';
import { Combobox } from '@camp/combobox';
// Example data structure - each option needs an 'ai' property
const teamMembers = [
{ name: 'Alice Johnson', team: 'Engineering', ai: true },
{ name: 'Bob Smith', team: 'Design', ai: false },
{ name: 'Carol Martinez', team: 'Engineering', ai: true },
{ name: 'David Chen', team: 'Product', ai: false },
];
export const MultiselectWithAIChips = () => {
const [allOptions, setAllOptions] = useState(teamMembers);
const [currentOptions, setCurrentOptions] = useState(teamMembers);
const [selected, setSelected] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const filterOptions = (inputValue?: string) => {
const searchTerm = inputValue?.trim()?.toLowerCase();
return allOptions.filter(
(option) =>
!searchTerm || option.name.toLowerCase().includes(searchTerm)
);
};
const handleSelect = (option) => {
if (!option) return;
// Reset input and options
setInputValue('');
setCurrentOptions(allOptions);
// Add to selected if not already present
const hasOption = selected.find((i) => i === option);
if (!hasOption) {
setSelected([...selected, option]);
}
};
const handleDeselect = (optionToRemove) => {
const newSelected = selected.filter((option) => option !== optionToRemove);
setSelected(newSelected);
};
const handleInputChange = ({ inputValue }) => {
setInputValue(inputValue as string);
setCurrentOptions(filterOptions(inputValue));
};
return (
<Combobox
chipColor="midnight" // must use midnight for use with AiChip
chipSize="large"
deselectOption={handleDeselect}
inputAriaLabel="Select team members"
inputValue={inputValue}
multiselect
onInputValueChange={handleInputChange}
onSelect={handleSelect}
options={currentOptions}
optionToString={(option) => option?.value?.name as string}
placeholder="Choose team members"
selectedOptions={selected}
variant="filterable" // can be filterable or editable
/>
);
};Usage
Best practices
The primary action when using a dropdown is selecting an option from the list, while for the combobox it’s searching for the right option. A small dataset should use a dropdown without a filter, a medium dataset can add a search field to the dropdown to filter results (i.e. a list of campaigns), and large to infinite datasets (i.e. mail addresses) are a better fit for the comboboxes.
✅ DO
- Do use in large data sets where a user is more likely to search for their option by typing than scrolling.
🚫 DON’T
- Don’t use for small datasets where filtering is not needed, such as 10 items or less.
Accessibility
Keyboard support
- To open the combobox on focus, press
spaceto open the menu or start typing to search the menu - When the dropdown menu is open, use
↑or↓to navigate andenterto select an item within the list - In an editable combobox, if a user is inputting a custom input, pressing
enterwill save that input and create it as a new item Tabor (shift+tabto move backward) closes the combobox and moves focus