Combobox

Combobox combines a text input with a dropdown list, letting users type to filter options. Built on HeadlessUI with full keyboard navigation and WCAG 2.2 AA compliance.


Installation

npm install @libretexts/davis-react

Basic Usage

import { Combobox } from '@libretexts/davis-react';
import { useState } from 'react';

const frameworks = ['React', 'Vue', 'Angular', 'Svelte', 'Solid'];

export default function Example() {
  const [value, setValue] = useState<string | null>(null);
  const [query, setQuery] = useState('');

  const filtered = query === ''
    ? frameworks
    : frameworks.filter(f => f.toLowerCase().includes(query.toLowerCase()));

  return (
    <Combobox value={value} onChange={setValue}>
      <Combobox.Label>Framework</Combobox.Label>
      <Combobox.Input
        placeholder="Search frameworks…"
        displayValue={(v) => v ?? ''}
        onChange={(e) => setQuery(e.target.value)}
      />
      <Combobox.Options>
        {filtered.length === 0 ? (
          <Combobox.Empty />
        ) : (
          filtered.map((f) => (
            <Combobox.Option key={f} value={f}>{f}</Combobox.Option>
          ))
        )}
      </Combobox.Options>
    </Combobox>
  );
}

Object Values

Use the by prop to compare objects by a specific key instead of by reference.

type Framework = { id: string; name: string };

const options: Framework[] = [
  { id: 'react', name: 'React' },
  { id: 'vue', name: 'Vue' },
  { id: 'angular', name: 'Angular' },
];

const [selected, setSelected] = useState<Framework | null>(null);
const [query, setQuery] = useState('');

const filtered = query === ''
  ? options
  : options.filter(f => f.name.toLowerCase().includes(query.toLowerCase()));

<Combobox value={selected} onChange={setSelected} by="id">
  <Combobox.Label>Framework</Combobox.Label>
  <Combobox.Input
    displayValue={(f) => f?.name ?? ''}
    onChange={(e) => setQuery(e.target.value)}
  />
  <Combobox.Options>
    {filtered.map((f) => (
      <Combobox.Option key={f.id} value={f}>{f.name}</Combobox.Option>
    ))}
  </Combobox.Options>
</Combobox>

Multiple Selection

Add multiple to allow selecting more than one value. value becomes an array.

const [selected, setSelected] = useState<string[]>([]);

<Combobox value={selected} onChange={setSelected} multiple>
  <Combobox.Label>Frameworks</Combobox.Label>
  <Combobox.Input
    displayValue={(v) => (v as string[]).join(', ')}
    onChange={(e) => setQuery(e.target.value)}
  />
  <Combobox.Options>
    {filtered.map((f) => (
      <Combobox.Option key={f} value={f}>{f}</Combobox.Option>
    ))}
  </Combobox.Options>
</Combobox>

Disabled Options

Add disabled to individual options to prevent selection and skip them in keyboard navigation.

<Combobox.Options>
  <Combobox.Option value="react">React</Combobox.Option>
  <Combobox.Option value="angular" disabled>Angular (deprecated)</Combobox.Option>
  <Combobox.Option value="vue">Vue</Combobox.Option>
</Combobox.Options>

Sizes

The size prop on Combobox.Input controls the input height and text size.

<Combobox value={value} onChange={setValue}>
  <Combobox.Label>Small</Combobox.Label>
  <Combobox.Input size="sm" displayValue={(v) => v ?? ''} onChange={handleChange} />
  <Combobox.Options>{/* ... */}</Combobox.Options>
</Combobox>

<Combobox value={value} onChange={setValue}>
  <Combobox.Label>Medium (default)</Combobox.Label>
  <Combobox.Input size="md" displayValue={(v) => v ?? ''} onChange={handleChange} />
  <Combobox.Options>{/* ... */}</Combobox.Options>
</Combobox>

<Combobox value={value} onChange={setValue}>
  <Combobox.Label>Large</Combobox.Label>
  <Combobox.Input size="lg" displayValue={(v) => v ?? ''} onChange={handleChange} />
  <Combobox.Options>{/* ... */}</Combobox.Options>
</Combobox>

No Results

Use Combobox.Empty to display a message when no options match the current query.

<Combobox.Options>
  {filtered.length === 0 ? (
    <Combobox.Empty>No frameworks found.</Combobox.Empty>
  ) : (
    filtered.map((f) => <Combobox.Option key={f} value={f}>{f}</Combobox.Option>)
  )}
</Combobox.Options>

Props

Combobox

PropTypeDefaultDescription
valueT | nullrequiredCurrently selected value
onChange(value: T | null) => voidrequiredCalled when selection changes
disabledbooleanfalseDisables the entire combobox
multiplebooleanfalseAllows selecting multiple values (value becomes T[])
nullablebooleanfalseAllows clearing the selection to null
namestringHTML form field name for native form submission
bykeyof T | (a: T, b: T) => booleanKey or function to compare option values
onClose() => voidCalled when the dropdown closes
classNamestringAdditional CSS classes on the wrapper

Combobox.Label

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Combobox.Input

PropTypeDefaultDescription
displayValue(value: T | null) => stringString(v)Converts selected value to display string
onChange(e: ChangeEvent) => voidHandle text input changes (use for filtering)
placeholderstringPlaceholder text
size'sm' | 'md' | 'lg''md'Controls input height and text size
aria-labelstringAccessible label when no visible Combobox.Label is used
classNamestringAdditional CSS classes

Combobox.Options

PropTypeDefaultDescription
classNamestringAdditional CSS classes

Combobox.Option

PropTypeDefaultDescription
valueTrequiredThe option value, passed to onChange on selection
disabledbooleanfalsePrevents selection and skips in keyboard navigation
classNamestringAdditional CSS classes

Combobox.Empty

PropTypeDefaultDescription
childrenReactNode"No results found."Message to display
classNamestringAdditional CSS classes

Accessibility

Combobox implements the ARIA combobox pattern. HeadlessUI manages all ARIA attributes automatically.

Keyboard navigation:

KeyAction
/ Navigate through options
EnterSelect the focused option
EscapeClose the dropdown
TabMove focus out of the combobox

Best practices:

  • Always provide either Combobox.Label or an aria-label on Combobox.Input so screen readers can identify the field.
  • Reset the query state in onClose so the full list is shown on the next open: <Combobox onClose={() => setQuery('')}>.
  • Avoid filtering on every keystroke for large remote datasets — debounce the onChange handler.

When to use

  • Combobox — Use when the list has many options (20+) or when users benefit from typing to filter. Ideal for country selectors, user lookup, and tag assignment.
  • Listbox — Use when you need custom option rendering (icons, badges) but don't need filtering.
  • Select — Use for short, static lists (5–15 items) where native browser styling is acceptable.