Skip to content
LogoLogo

Combobox

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

Loading...

Loading...

Overview

Resources

Loading...

Loading...

Loading...

Loading...

Install

yarn add @camp/combobox

Props

PropTypeDefaultDescription
inputAriaLabel *stringSpecify the aria-label to apply to the Combobox input
onInputValueChange *(changes: UseComboboxInputValueChange<OptionProps<T>>) => voidProvide a function to filter the items in the list according to the input value
options *T[]Provide an array of options to be displayed in the dropdown. If using multiselect with some options reflecting AI suggestions, include a boolean `ai` property on the option object to render selected options as AI chips.
optionToString *(option: OptionProps<T>) => stringSpecify the text context for your combobox `option` @param option OptionProps<T> @returns string
variant *"filterable" | "editable"Specify if the input field be used only for filtering options or as a source of creating new values as well - can be either `filterable` or `editable`
onSelect(option?: T) => voidOptionally specify an `onSelect` callback if using as controlled component @param option T @returns void
inputValuestringThe controlled input value passed from the parent component
placeholderstringPlaceholder value inside input
inputWidthstringThe width of the input trigger. Accepts any valid CSS width value (e.g., "200px", "100%", "auto"). If not specified, the input will fill its container (100% width).
renderOption(option: OptionProps<T>) => ReactNodeRender callback for rendering content of option in list; not needed if you're rendering a list of strings. Use the `highlightedIndex` & `isSelected` params to conditionally style options when they should display as selected or highlighted. @param option @param isSelected @param highlightedIndex @returns React.ReactNode
themedbooleanWhether the combobox should adapt to light/dark theme modes. Combobox does not contain dark mode theme styles yet, but this prop is here for future compatibility.
disabledbooleanDisplay disabled state
invalidbooleanDisplay invalid state
labelstringOptionally specify a label to display above the input
deselectOption(option: T) => voidCallback function to remove a selected option from multiselect combobox
multiselectbooleanSet to true for multiselect combobox
noResultsFallbackstringNo results fallback in case translation doesn't load. This is a t() problem, so if that gets fixed, we don't need this.
onBlurCaptureFocusEventHandler<HTMLDivElement>
onClickMouseEventHandler<HTMLDivElement>
onCreate(value: string) => voidOptionally specify a 'onCreate' callback with access to the inputs `value` when 'Create' option is selected @param value @returns void
onFocusCaptureFocusEventHandler<HTMLDivElement>
onInputCaptureFormEventHandler<HTMLDivElement>
onKeyDownCaptureKeyboardEventHandler<HTMLDivElement>
onMouseDownCaptureMouseEventHandler<HTMLDivElement>
renderMenuActions() => ReactNodeOptional render function for actions at the bottom of the menu (e.g., "Create" button) Rendered below the scrollable options list with a visual separator
selectedOptionsT[]For use with multiselect. The current array of selected options.
styleCSSProperties
chipColoranyThe color of the chips in the multiselect combobox. Defaults to 'ac-blue'. Use 'midnight' when including menu options that are AI-generated.
chipSizeChipSizeThe size of the chips in the multiselect combobox. Defaults to 'small'.
onNoResults() => voidOptionally specify an `onNoResults` callback if using as controlled component when 'No results' option is selected @returns void
refRef<HTMLDivElement>

Generated from @camp/combobox@8.16.0 source, ordered by production usage. * required. † standard HTML attribute — all native attributes are supported; only those with production usage are listed.

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
Copyright © 2026 ActiveCampaign