Type to filter, pick to commit. For lists too long to scan.
| Prop / event | Type | Notes |
|---|---|---|
| options | Item[] | The full set to filter over |
| onSelect | (item) => void | Fires when an option is committed |
| filter | (item, q) => boolean | Custom matcher; defaults to substring |
| emptyState | ReactNode | Shown when nothing matches |
| ↑ / ↓ · Enter | — | Move the active option; Enter commits it |
| Esc · outside click | — | Close the list |
Use a Combobox when the list is too long to scan but each pick is a known value. For free-text queries that return results use Search; for a short list use Dropdown.
Highlight the match so the reader sees why each result is here.
Don't just dump the whole list unfiltered. Then it is only a Dropdown.
Give the no-match case an honest, specific message.
Don't leave an empty box open. Silence reads as broken.
Long lists that a plain Dropdown would drown in.
Assigning people by typing part of a name or email.
Choosing from a controlled vocabulary of tags or labels.
| Token | Value | Role |
|---|---|---|
| --combo-match | #FF9D45 | Highlighted substring, weight 600 |
| --combo-active | #1E1F25 | Keyboard-active row |
| --menu-max-h | 244px | Listbox scrolls past this |
| --menu-bg | #141519 | Results panel |
| --empty-color | #767B87 | Empty-state text |
| --field-focus | rgba(255,122,0,.14) | Warm focus ring on the input |
<div class="o-combo">
<input class="o-input" role="combobox" aria-expanded="true"
aria-autocomplete="list" placeholder="Search fruit…" />
<div class="o-menu" role="listbox">
<button role="option" class="o-menu__opt is-active">
<b class="o-combo__match">Ap</b>ple
</button>
<button role="option" class="o-menu__opt"><b class="o-combo__match">Ap</b>ricot</button>
</div>
</div>