Combobox with Add New

Adding the ability to create new options in a Combobox when none are found


In some cases, you may want to allow users to create new options in a Combobox when they can't find what they're looking for. This is useful for fields like tags, categories, or any user-defined values where the list of options should be dynamic.

The pattern below shows how to add a "+ Create" option to the Combobox dropdown that appears:

  • At the bottom of the list when the user has typed something, or
  • Only when no matching results are found

Implementation Notes

The key aspects of this pattern:

  • Add a special option with a sentinel value (e.g., __add_new__) to your options array
    • This is a unique identifier that won't conflict with real option values
    • It lets you detect when the user selected "Create" instead of a regular option
  • In the onSelectionChange handler, check if the selected value matches the sentinel and handle it differently
  • When creating a new option:
    • Generate a unique value for the new option
    • Add it to your options list
    • Select the newly created option
    • Update the input value to show the new option's label

Show Create Option When User Types

This example shows the "+ Create" option at the bottom of the list whenever the user has typed something:

import { Combobox } from '@customerio/pluma-components/react';
import { useState, useMemo } from 'react';

export default function Example() {
  const [inputValue, setInputValue] = useState('');
  const [selectedValue, setSelectedValue] = useState(null);

  const [baseOptions, setBaseOptions] = useState([
    { value: 'javascript', label: 'JavaScript' },
    { value: 'typescript', label: 'TypeScript' },
    { value: 'python', label: 'Python' },
    { value: 'ruby', label: 'Ruby' },
  ]);

  // Add "Create" option to the list when there's input
  const optionsWithAdd = useMemo(() => {
    const options = [...baseOptions];

    if (inputValue.trim()) {
      options.push({
        value: '__add_new__',
        label: `+ Create "${inputValue}"`
      });
    }

    return options;
  }, [baseOptions, inputValue]);

  const handleSelectionChange = (value) => {
    if (value === '__add_new__') {
      handleCreateNew(inputValue);
    } else {
      setSelectedValue(value);
      // Update input to show the selected option's label
      const selectedOption = baseOptions.find(opt => opt.value === value);
      if (selectedOption) {
        setInputValue(selectedOption.label);
      }
    }
  };

  const handleCreateNew = (newLabel) => {
    if (!newLabel || newLabel.trim() === '') return;

    // Create new option with a unique value
    const newOption = {
      value: newLabel.toLowerCase().replace(/s+/g, '_'),
      label: newLabel
    };

    // Add to options list
    setBaseOptions([...baseOptions, newOption]);

    // Select the new option
    setSelectedValue(newOption.value);
    setInputValue(newOption.label);
  };

  return (
    <Combobox
      label="Programming Language"
      placeholder="Select or type to search..."
      options={optionsWithAdd}
      selectedValue={selectedValue}
      inputValue={inputValue}
      onSelectionChange={handleSelectionChange}
      onValueChange={setInputValue}
    />
  );
}

Show Create Only When No Results

This example only shows the "+ Create" option when the search returns no matching results:

import { Combobox } from '@customerio/pluma-components/react';
import { useState, useMemo } from 'react';

export default function Example() {
  const [inputValue, setInputValue] = useState('');
  const [selectedValue, setSelectedValue] = useState(null);

  const [baseOptions, setBaseOptions] = useState([
    { value: 'javascript', label: 'JavaScript' },
    { value: 'typescript', label: 'TypeScript' },
    { value: 'python', label: 'Python' },
    { value: 'ruby', label: 'Ruby' },
  ]);

  const optionsWithAdd = useMemo(() => {
    // Combobox filters automatically, so we check if any options match
    const hasMatches = baseOptions.some(opt =>
      opt.label.toLowerCase().includes(inputValue.toLowerCase())
    );

    // Only add "Create" option when:
    // 1. User has typed something
    // 2. No existing options match
    if (!hasMatches && inputValue.trim().length > 0) {
      return [
        {
          value: '__add_new__',
          label: `+ Create "${inputValue}"`
        }
      ];
    }

    return baseOptions;
  }, [baseOptions, inputValue]);

  const handleSelectionChange = (value) => {
    if (value === '__add_new__') {
      handleCreateNew(inputValue);
    } else {
      setSelectedValue(value);
      // Update input to show the selected option's label
      const selectedOption = baseOptions.find(opt => opt.value === value);
      if (selectedOption) {
        setInputValue(selectedOption.label);
      }
    }
  };

  const handleCreateNew = (newLabel) => {
    if (!newLabel || newLabel.trim() === '') return;

    const newOption = {
      value: newLabel.toLowerCase().replace(/s+/g, '_'),
      label: newLabel
    };

    setBaseOptions([...baseOptions, newOption]);
    setSelectedValue(newOption.value);
    setInputValue(newOption.label);
  };

  return (
    <Combobox
      label="Programming Language"
      placeholder="Select or type to search..."
      options={optionsWithAdd}
      selectedValue={selectedValue}
      inputValue={inputValue}
      onSelectionChange={handleSelectionChange}
      onValueChange={setInputValue}
    />
  );
}

Persisting New Options

In a real application, you'll likely want to persist newly created options to a backend:

const handleCreateNew = async (newLabel) => {
  if (!newLabel || newLabel.trim() === '') return;

  // Show loading state
  setIsLoading(true);

  try {
    // Create option in backend
    const response = await fetch('/api/options', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ label: newLabel })
    });

    const newOption = await response.json();

    // Add to local state (use functional update to avoid stale closure)
    setBaseOptions(prev => [...prev, newOption]);
    setSelectedValue(newOption.value);
    setInputValue(newOption.label);
  } catch (error) {
    console.error('Failed to create option:', error);
    // Handle error (show toast, etc.)
  } finally {
    setIsLoading(false);
  }
};