Combobox

July 6, 2026 · View on GitHub

Select with an embedded text input for filtering options. Single and multiple selection.

Contract

<kp-combobox> is a Select-shaped control where the trigger is a real text field. Typing into it narrows the option list via case-insensitive substring match; matches are highlighted. Pick with click, Enter, or Tab. Shares the Input size ramp (xs / sm / md / lg / xl) so it composes with the rest of the form grammar.

Anatomy

$ \text{Combobox} ├─ \text{Trigger} (\text{focuses} \text{on} \text{click}, \text{holds} \text{the} \text{query} \text{text}) │ ├─ \text{Multi}-\text{summary} — "3 \text{selected}" \text{when} \text{closed} \text{in} \text{multi} \text{mode} │ ├─ \text{Input} — \text{filter} \text{query} + \text{aria}-\text{expanded} / \text{aria}-\text{activedescendant} │ ├─ \text{Clear} — \times \text{button} (\text{when} \text{a} \text{value} \text{is} \text{set}) │ └─ \text{Chevron} — \text{rotates} \text{when} \text{open} └─ \text{Dropdown} (\text{listbox}, \text{appears} \text{below} \text{trigger}) ├─ \text{Option} — \text{with} \text{optional} \text{checkbox} (\text{multi}) + <\text{mark}>-\text{highlighted} \text{match} └─ \text{Empty} \text{state} — "\text{No} \text{results} \text{found}" $

API

Inputs

NameTypeDefaultDescription
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Size ramp (inherits Input's grammar)
optionsKpComboboxOption[][]{ value, label, disabled? } list
placeholderstring'Search or select…'Shown when no value
emptyMessagestring'No results found'Shown when the filtered list is empty
multiplebooleanfalseMulti-select mode
showClearbooleantrueShow the × clear affordance
disabledbooleanfalseDisable the whole control
ariaLabelstring''Accessible label forwarded to the <input>
forceStateKpState | nullnullPin visual state (hover, focus, error, disabled) for docs

Outputs

NamePayloadFires when
openChangebooleanDropdown opens / closes
queryChangestringUser types in the filter input

Forms

Implements ControlValueAccessor. In single mode ngModel is a string \| null; in multi mode it's string[].

<kp-combobox [options]="fruits" [(ngModel)]="fruit"/>
<kp-combobox [multiple]="true" [options]="tags" [(ngModel)]="picked"/>

States

StateBehavior
restNeutral border, placeholder visible
hoverBorder darkens
focus / openBorder turns blue; dropdown visible when open
disabledMuted background, no interaction
errorRed border

Accessibility

  • Roles: role="combobox" on the <input>, role="listbox" on the dropdown, role="option" on each row.
  • ARIA:
    • aria-expanded tracks open state
    • aria-autocomplete="list"
    • aria-controls + matching id on the listbox
    • aria-activedescendant points to the current keyboard-highlighted option
    • aria-selected on selected options; aria-multiselectable on the listbox when multiple
  • Keyboard:
    • ↓ / ↑ — move the highlight; skips disabled options
    • Home / End — jump to first / last
    • Enter — pick the highlighted option
    • Escape — close the dropdown and clear the query
    • Backspace on an empty query in multi mode — remove the last picked tag
  • Click-outside closes the dropdown; focus returns naturally to where the user clicked.

Do / Don't

Do

  • Pair long lists of known values (countries, tags, SKUs) with Combobox — the filter makes them scannable.
  • Provide a short placeholder that hints at the search, e.g. "Search countries…".
  • Use multiple when users routinely pick more than one value; otherwise stick with single.

Don't

  • Don't use Combobox for free-form input — it snaps to known options on commit. If users need to type arbitrary strings, use <kp-input> + a suggestions menu.
  • Don't put more than ~500 options in the default (synchronous) mode. For async / remote results, render options from your query observable and let the component filter the displayed slice.
  • Don't hide the clear affordance when a value is set — users expect to reset without opening a menu.

References

Changelog

  • 0.1.0 — Initial release. Single + multi, 5 sizes, keyboard navigation, match-highlighting, full CVA support.