# Combobox

> An editable input with a filtered list of suggestions, supporting async search and custom values.

Category: Forms · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/combobox

Use Combobox when the list is long enough that people need to type to find an option, such as assigning a template to one of hundreds of organisation users. It filters as you type, can search a server (filter={false} with a controlled input), and can accept free text with allowCustomValue. For short fixed lists use Select.

## Import

```tsx
import { Combobox, type SelectOptions } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Basic

```tsx
import { Combobox, FormField, Icon } from '@bindshq/stance-react';
import type { SelectOptions } from '@bindshq/stance-react';

const members: SelectOptions = [
  { value: 'u1', label: 'Amara Okafor', description: 'Legal · amara@binds.si' },
  { value: 'u2', label: 'Ben Hughes', description: 'Sales · ben@binds.si' },
  { value: 'u3', label: 'Chen Wei', description: 'Finance · chen@binds.si' },
  { value: 'u4', label: 'Dana Ruiz', description: 'Marketing · dana@binds.si' },
  { value: 'u5', label: 'Elif Demir', description: 'Support · elif@binds.si' },
  { value: 'u6', label: 'Farah Khan', description: 'Suspended', disabled: true },
];

export function Basic() {
  return (
    <div style={{ width: 360 }}>
      <FormField label="Assign signature to" description="Search your organisation’s users.">
        <Combobox options={members} placeholder="Type a name" prefix={<Icon name="search" />} />
      </FormField>
    </div>
  );
}
```

### Async search

Disable local filtering and drive options and loading from your API.

```tsx
import * as React from 'react';
import { Combobox, FormField } from '@bindshq/stance-react';
import type { SelectOptions } from '@bindshq/stance-react';

const members: SelectOptions = [
  { value: 'u1', label: 'Amara Okafor', description: 'Legal · amara@binds.si' },
  { value: 'u2', label: 'Ben Hughes', description: 'Sales · ben@binds.si' },
  { value: 'u3', label: 'Chen Wei', description: 'Finance · chen@binds.si' },
  { value: 'u4', label: 'Dana Ruiz', description: 'Marketing · dana@binds.si' },
  { value: 'u5', label: 'Elif Demir', description: 'Support · elif@binds.si' },
  { value: 'u6', label: 'Farah Khan', description: 'Suspended', disabled: true },
];

export function AsyncSearch() {
  const [text, setText] = React.useState('');
  const [results, setResults] = React.useState<SelectOptions>([]);
  const [loading, setLoading] = React.useState(false);
  React.useEffect(() => {
    if (!text) {
      setResults([]);
      return;
    }
    setLoading(true);
    // Stand-in for a debounced API call.
    const timer = setTimeout(() => {
      const q = text.toLowerCase();
      setResults(
        members.filter(
          (m) => 'label' in m && !('options' in m) && m.label.toLowerCase().includes(q),
        ),
      );
      setLoading(false);
    }, 400);
    return () => clearTimeout(timer);
  }, [text]);
  return (
    <div style={{ width: 360 }}>
      <FormField label="Find a user">
        <Combobox
          options={results}
          filter={false}
          inputValue={text}
          onInputChange={setText}
          loading={loading}
          emptyText={text ? 'No users found' : 'Start typing to search'}
        />
      </FormField>
    </div>
  );
}
```

### Custom values

Accept text that matches no option.

```tsx
import * as React from 'react';
import { Combobox, FormField } from '@bindshq/stance-react';

export function CustomValue() {
  const [value, setValue] = React.useState<string | null>(null);
  return (
    <div style={{ display: 'grid', gap: 8, width: 360 }}>
      <FormField label="Contract category">
        <Combobox
          allowCustomValue
          value={value}
          onValueChange={setValue}
          options={[
            { value: 'nda', label: 'Non-disclosure agreement' },
            { value: 'msa', label: 'Master services agreement' },
            { value: 'sow', label: 'Statement of work' },
          ]}
        />
      </FormField>
      <span style={{ color: 'var(--st-color-fg-muted)', fontSize: 'var(--st-font-size-sm)' }}>
        Value: {value ?? 'none'}
      </span>
    </div>
  );
}
```

### Error and disabled

```tsx
import { Combobox, FormField } from '@bindshq/stance-react';
import type { SelectOptions } from '@bindshq/stance-react';

const members: SelectOptions = [
  { value: 'u1', label: 'Amara Okafor', description: 'Legal · amara@binds.si' },
  { value: 'u2', label: 'Ben Hughes', description: 'Sales · ben@binds.si' },
  { value: 'u3', label: 'Chen Wei', description: 'Finance · chen@binds.si' },
  { value: 'u4', label: 'Dana Ruiz', description: 'Marketing · dana@binds.si' },
  { value: 'u5', label: 'Elif Demir', description: 'Support · elif@binds.si' },
  { value: 'u6', label: 'Farah Khan', description: 'Suspended', disabled: true },
];

export function States() {
  return (
    <div style={{ display: 'grid', gap: 16, width: 360 }}>
      <FormField label="Owner" error="Choose an owner for this template" required>
        <Combobox options={members} />
      </FormField>
      <FormField label="Created by" disabled>
        <Combobox options={members} defaultValue="u1" />
      </FormField>
    </div>
  );
}
```

## API reference

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| Combobox | `options` (required) | `SelectOptions` | None | Options or groups, as in Select. |
| Combobox | `value / defaultValue` | `string \| null` | `null` | Selected value (controlled or initial). |
| Combobox | `onValueChange` | `(value: string \| null) => void` | None | Called when a value is chosen or cleared. |
| Combobox | `inputValue / defaultInputValue` | `string` | None | Text in the input (controlled or initial). |
| Combobox | `onInputChange` | `(text: string) => void` | None | Called as the user types. Trigger async searches here. |
| Combobox | `filter` | `boolean \| (option, query) => boolean` | `true` | Local filtering; false for server-side search. |
| Combobox | `allowCustomValue` | `boolean` | `false` | Accept unmatched text as the value. |
| Combobox | `loading / loadingText` | `boolean / ReactNode` | `false / 'Loading…'` | Shows a loading row and aria-busy. |
| Combobox | `emptyText` | `ReactNode` | `'No results'` | Shown when nothing matches. |
| Combobox | `openOnFocus` | `boolean` | `false` | Open the list when focused. |
| Combobox | `clearable` | `boolean` | `true` | Show a clear button when there is text. |
| Combobox | `prefix` | `ReactNode` | None | Leading content inside the field. |
| Combobox | `name / form` | `string` | None | Submits the selected value through a hidden input. |
| Combobox | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Control height. |
| Combobox | `open / defaultOpen / onOpenChange` | `boolean / function` | None | Popup open state. |
| Combobox | `renderOption` | `(option, state) => ReactNode` | None | Custom option content. |

## Accessibility

- The input has role="combobox", aria-autocomplete="list", aria-expanded and aria-controls (WAI-ARIA APG).
- The active suggestion is announced through aria-activedescendant while focus stays in the input.
- Loading and empty states are announced through role="status"; the list sets aria-busy while loading.
- Without allowCustomValue, unmatched text reverts to the selected option on blur, so the value is never ambiguous.

## Keyboard

| Keys | Action |
| --- | --- |
| `ArrowDown` | Open the list, or move to the next suggestion (wraps). |
| `ArrowUp` | Open at the last suggestion, or move to the previous one (wraps). |
| `Alt+ArrowDown` | Open the list without moving the active suggestion. |
| `Enter` | Choose the active suggestion, or commit typed text. |
| `Escape` | Close the list; when closed, clear the input. |
| `Tab` | Choose the active suggestion after typing, and move on. |

## Guidelines

**Do**

- Debounce async searches by around 200ms.
- Use descriptions to tell similar options apart, such as email addresses.

**Don’t**

- Use Combobox for short lists where Select is faster.
- Allow custom values when only listed values are valid.

## Related

- https://stanceui.com/components/select
- https://stanceui.com/components/multi-select
- https://stanceui.com/components/search-input
- https://stanceui.com/components/command-menu
