Components Forms
Combobox
StableAn editable input with a filtered list of suggestions, supporting async search and custom values.
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.
Overview
Search your organisation’s users.
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>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
Search your organisation’s users.
Search your organisation’s users.
Search your organisation’s users.
Import
import { Combobox, type SelectOptions } from '@bindshq/stance-react';Examples
Async search
Disable local filtering and drive options and loading from your API.
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.
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>
);
}Choose an owner for this template
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
| Prop | Type | Default | Description |
|---|---|---|---|
options* required | SelectOptions | None | Options or groups, as in Select. |
value / defaultValue | string | null | null | Selected value (controlled or initial). |
onValueChange | (value: string | null) => void | None | Called when a value is chosen or cleared. |
inputValue / defaultInputValue | string | None | Text in the input (controlled or initial). |
onInputChange | (text: string) => void | None | Called as the user types. Trigger async searches here. |
filter | boolean | (option, query) => boolean | true | Local filtering; false for server-side search. |
allowCustomValue | boolean | false | Accept unmatched text as the value. |
loading / loadingText | boolean / ReactNode | false / 'Loading…' | Shows a loading row and aria-busy. |
emptyText | ReactNode | 'No results' | Shown when nothing matches. |
openOnFocus | boolean | false | Open the list when focused. |
clearable | boolean | true | Show a clear button when there is text. |
prefix | ReactNode | None | Leading content inside the field. |
name / form | string | None | Submits the selected value through a hidden input. |
size | 'sm' | 'md' | 'lg' | 'md' | Control height. |
open / defaultOpen / onOpenChange | boolean / function | None | Popup open state. |
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. |
Usage 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.