Skip to Content
DocumentationComponentsComboboxNext Gen

Combobox

A powerful dropdown for providing suggestions, filtering out large datasets, and creating new values quickly and efficiently with no overhead.

Loading...

Overview

Resources

Loading...

Loading...

Loading...

Loading...

Install

yarn add @camp/combobox

Upgrading 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

  1. Replace imports: Replace the old Combobox imports with the Combobox from @camp/combobox.
  2. 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

Combobox 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.

Combobox with AiChip
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

Combobox usage

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 space to open the menu or start typing to search the menu
  • When the dropdown menu is open, use or to navigate and enter to select an item within the list
  • In an editable combobox, if a user is inputting a custom input, pressing enter will save that input and create it as a new item
  • Tab or (shift + tab to move backward) closes the combobox and moves focus
Last updated on