Combobox

A searchable selection component that combines a text input with a dropdown list, allowing users to filter and select from available options

Installation

pnpm add @wandercom/design-system-web

Usage

28 lines
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from '@wandercom/design-system-web/ui/combobox';

const options = ['Option 1', 'Option 2', 'Option 3'];

export function Example() {
  return (
    <Combobox items={options}>
      <ComboboxInput placeholder="Search..." />
      <ComboboxContent>
        <ComboboxEmpty>No results found.</ComboboxEmpty>
        <ComboboxList>
          {(item) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

Examples

The default example shows a basic combobox with type-to-filter, a variant with the clear button enabled, and the disabled state.

Loading example...
42 lines
const destinations = ['Aspen', 'Big Bear', 'Joshua Tree', 'Lake Tahoe', 'Malibu'];

<Combobox items={destinations}>
  <ComboboxInput placeholder="Search destinations..." />
  <ComboboxContent>
    <ComboboxEmpty>No destinations found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

<Combobox items={destinations}>
  <ComboboxInput placeholder="With clear button" showClear />
  <ComboboxContent>
    <ComboboxEmpty>No destinations found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

<Combobox disabled items={destinations}>
  <ComboboxInput placeholder="Disabled combobox" />
  <ComboboxContent>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

With object values

When item values are objects with a { value, label } shape, Base UI automatically uses label for display and filtering, while value is used for form submission. This is useful when the display text differs from the underlying ID.

Loading example...
19 lines
const destinations = [
  { value: 'aspen-co', label: 'Aspen, Colorado' },
  { value: 'big-bear-ca', label: 'Big Bear, California' },
  { value: 'joshua-tree-ca', label: 'Joshua Tree, California' },
];

<Combobox items={destinations}>
  <ComboboxInput placeholder="Search destinations..." />
  <ComboboxContent>
    <ComboboxEmpty>No destinations found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item.value} value={item}>
          {item.label}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Controlled

When managing the selected value externally, pass both value/onValueChange and inputValue/onInputValueChange. Base UI updates the input text via onInputValueChange when an item is selected, so both callbacks are required for the input to reflect the selection.

Loading example...
22 lines
const [value, setValue] = useState<Destination | null>(null);
const [inputValue, setInputValue] = useState('');

<Combobox
  inputValue={inputValue}
  items={destinations}
  value={value}
  onInputValueChange={setInputValue}
  onValueChange={setValue}
>
  <ComboboxInput placeholder="Search destinations..." />
  <ComboboxContent>
    <ComboboxEmpty>No destinations found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item.value} value={item}>
          {item.label}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Scrollable

Long lists scroll inside the popup, which is capped at 500px (or the available viewport height, whichever is smaller). Chevron indicators appear over the list edges while more content is available in that direction. Pass scrollable={false} to ComboboxContent to remove the 500px cap and the indicators — the popup then grows to the full available height and the list still scrolls if it runs out of room.

Loading example...
29 lines
const countries = ['Argentina', 'Australia', 'Austria' /* ... 30+ items */];

<Combobox items={countries}>
  <ComboboxInput placeholder="Search countries..." />
  <ComboboxContent>
    <ComboboxEmpty>No countries found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

<Combobox items={countries}>
  <ComboboxInput placeholder="Search countries..." />
  <ComboboxContent scrollable={false}>
    <ComboboxEmpty>No countries found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Truncation

By default an item grows to fit its label, wrapping long text onto multiple lines. Pass truncate to ComboboxItem to pin the row to a single line and ellipsize overflowing text instead. Truncation is intended for plain text labels — labels that combine icons and text should use the default growing rows.

Loading example...
35 lines
const properties = [
  'Aspen Chalet',
  'Big Bear Lakefront Lodge with Private Dock and Mountain Views',
  /* ... */
];

// Default: rows grow and long labels wrap
<Combobox items={properties}>
  <ComboboxInput placeholder="Search properties..." />
  <ComboboxContent>
    <ComboboxEmpty>No properties found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

// truncate: rows stay single-line and ellipsize
<Combobox items={properties}>
  <ComboboxInput placeholder="Search properties..." />
  <ComboboxContent>
    <ComboboxEmpty>No properties found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} truncate value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

With groups

Use ComboboxGroup, ComboboxLabel, and ComboboxSeparator to organize options into labeled sections.

Loading example...
26 lines
const regionGroups = [
  { value: 'Mountains', items: ['Aspen', 'Big Bear', 'Telluride'] },
  { value: 'Desert', items: ['Joshua Tree', 'Palm Springs', 'Sedona'] },
  { value: 'Coast', items: ['Malibu', 'Carmel', 'Cannon Beach'] },
];

<Combobox items={regionGroups}>
  <ComboboxInput placeholder="Search destinations..." />
  <ComboboxContent>
    <ComboboxEmpty>No destinations found.</ComboboxEmpty>
    <ComboboxList>
      {(group) => (
        <ComboboxGroup items={group.items} key={group.value}>
          <ComboboxLabel>{group.value}</ComboboxLabel>
          <ComboboxCollection>
            {(item) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxCollection>
        </ComboboxGroup>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Props

Combobox

items:

Value[] | { value: string; items: Value[] }[]
The items to display and filter. Pass a flat array for a simple list, or an array of groups with nested items for grouped layouts. Base UI filters these items against the input value automatically.

value?:

Value | Value[]
The controlled selected value.

defaultValue?:

Value | Value[]
The default selected value when the component is first rendered.

onValueChange?:

(value: Value, eventDetails: ChangeEventDetails) => void
Callback fired when the selected value changes.

inputValue?:

string
The controlled input value. When provided, you must also pass onInputValueChange to keep the input in sync with selections.

onInputValueChange?:

(value: string, eventDetails: ChangeEventDetails) => void
Callback fired when the input value changes, including when an item is selected. Required when inputValue is controlled.

disabled?:

boolean
Disables the combobox when true.

multiple?:

boolean
Enables multi-select mode.

open?:

boolean
The controlled open state of the combobox popup.

onOpenChange?:

(open: boolean) => void
Callback fired when the open state changes.

ComboboxInput

placeholder?:

string
Placeholder text displayed when the input is empty.

showTrigger?:

boolean
Whether to show the dropdown trigger button. Defaults to true.

showClear?:

boolean
Whether to show the clear button. Defaults to false.

size?:

'default' | 'sm'
Height of the input. Defaults to 'default'.

triggerLabel?:

string
Accessible label for the icon-only trigger button. Defaults to 'Open popup'.

clearLabel?:

string
Accessible label for the clear button. Defaults to 'Clear selection'.

disabled?:

boolean
Disables the input.

className?:

string
Additional CSS classes to apply.

ComboboxContent

side?:

'top' | 'bottom' | 'left' | 'right'
Which side of the anchor to position against. Defaults to 'bottom'.

sideOffset?:

number
Offset from the anchor edge in pixels. Defaults to 6.

align?:

'start' | 'center' | 'end'
Alignment along the anchor edge. Defaults to 'start'.

alignOffset?:

number
Offset for alignment in pixels. Defaults to 0.

anchor?:

React.RefObject<HTMLElement>
Custom anchor element for positioning the popup.

scrollable?:

boolean
Caps the popup height at 500px (or the available viewport height, whichever is smaller) and shows scroll-overflow chevron indicators over the list edges. When false, the popup grows to the full available height without indicators. Defaults to true.

className?:

string
Additional CSS classes to apply.

ComboboxItem

value:

string
The unique value of this combobox item.

truncate?:

boolean
Pins the row to a single line and ellipsizes overflowing label text. When false, the row grows to fit wrapped text. Intended for plain text labels. Defaults to false.

disabled?:

boolean
Disables this specific item.

className?:

string
Additional CSS classes to apply.

ComboboxLabel

className?:

string
Additional CSS classes to apply to the label.

ComboboxEmpty

className?:

string
Additional CSS classes to apply.

ComboboxSeparator

className?:

string
Additional CSS classes to apply to the separator.

ComboboxChips

className?:

string
Additional CSS classes to apply to the chips container.

ComboboxChip

showRemove?:

boolean
Whether to show the remove button. Defaults to true.

removeLabel?:

string
Accessible label for the icon-only remove button. Defaults to 'Remove'.

className?:

string
Additional CSS classes to apply.

Accessibility

The Combobox component is built on the Base UI Combobox primitive, which implements the WAI-ARIA combobox pattern for accessible searchable selection.

  • Supports keyboard interaction: type to filter options, arrow keys to navigate the list, Enter to select the highlighted item, and Escape to close the popup
  • Screen reader announces the count of filtered results as the user types
  • Focus management ensures visible focus indicators during keyboard navigation
  • Disabled items apply data-disabled and are excluded from keyboard navigation
  • Multi-select mode uses chips with accessible remove buttons for deselecting values
  • Icon-only buttons ship with default English labels — "Open popup" on the trigger, "Clear selection" on the clear button, and "Remove" on chip remove buttons. Override them for localization via triggerLabel/clearLabel on ComboboxInput, aria-label on ComboboxTrigger/ComboboxClear, and removeLabel on ComboboxChip. The empty-state message defaults to "No items found." and is customized via ComboboxEmpty children
Combobox