Combobox composes field, dropdown, option, and the highlight pipe with the combobox directive.

combobox.ts
import {
  KuiComboboxDirective,
  KuiComboboxHighlightPipe,
  KuiDropdownComponent,
  KuiFieldComponent,
  KuiOptionDirective,
  kuiProvideComboboxOptions,
} from '@kikita-labs/ui';

Bind value and query with Angular signals. Filter and render projected options yourself.

Type to filter projected options

<kui-field label="Assignee" hint="Type to filter projected options">
  <input
    kuiCombobox
    [(value)]="assignee"
    [(query)]="query"
    [kuiLabelFn]="personLabel"
    placeholder="Search people..."
    (search)="query.set($event)"
  />

  <kui-dropdown>
    @for (person of filteredPeople(); track person.id) {
      <button kuiOption [value]="person">
        <span class="kui-combobox-match-label">
          @for (segment of person.name | kuiComboboxHighlight: query(); track $index) {
            @if (segment.match) {
              <mark class="kui-combobox-highlight">{{ segment.text }}</mark>
            } @else {
              <span>{{ segment.text }}</span>
            }
          }
        </span>
      </button>
    } @empty {
      <div class="kui-combobox-empty">No people found</div>
    }
  </kui-dropdown>
</kui-field>

Use (search) for remote requests. The directive does not own async data; update the projected options when results arrive. Use mode="async" to document that filtering happens outside the directive.

Simulates a remote lookup with a loading row

<kui-field label="Reviewer" hint="Simulates a remote lookup with a loading row">
  <input
    kuiCombobox
    mode="async"
    [(value)]="reviewer"
    [(query)]="reviewerQuery"
    [loading]="loading()"
    [kuiLabelFn]="personLabel"
    placeholder="Type to search..."
    (search)="loadReviewers($event)"
  />

  <kui-dropdown>
    @if (loading()) {
      <div class="kui-combobox-loading-row">
        <span class="kui-combobox-loader" aria-hidden="true"></span>
        Loading people
      </div>
    } @else if (errorMessage(); as lookupError) {
      <div class="kui-combobox-empty" role="alert">{{ lookupError }}</div>
    } @else {
      @for (person of reviewers(); track person.id) {
        <button kuiOption [value]="person">{{ person.name }}</button>
      } @empty {
        <div class="kui-combobox-empty">No matches</div>
      }
    }
  </kui-dropdown>
</kui-field>

mode="free" stores typed text as the value. In the default filter mode, editing text clears the selected value until a projected option is chosen.

Type a custom tag or choose a suggestion

<kui-field label="Tag" hint="Type a custom tag or choose a suggestion">
  <input kuiCombobox mode="free" [(value)]="tag" placeholder="Type or choose..." />
  <kui-dropdown>
    <button kuiOption value="Bug">Bug</button>
    <button kuiOption value="Feature">Feature</button>
    <button kuiOption value="Regression">Regression</button>
  </kui-dropdown>
</kui-field>

Combobox inherits field id, label association, aria-describedby, invalid state, and field size from kui-field.

Disabled prevents editing and opening the dropdown

<div class="combobox-field-states-example">
  <kui-field label="Owner" hint="Disabled prevents editing and opening the dropdown">
    <input kuiCombobox disabled [(value)]="disabledOwner" placeholder="Search people..." />
    <kui-dropdown>
      <button kuiOption value="engineer">Software Engineer</button>
      <button kuiOption value="designer">Designer</button>
    </kui-dropdown>
  </kui-field>

  <kui-field label="Owner" error="Choose an owner before continuing">
    <input kuiCombobox invalid [(value)]="invalidOwner" placeholder="Search people..." />
    <kui-dropdown>
      <button kuiOption value="engineer">Software Engineer</button>
      <button kuiOption value="designer">Designer</button>
    </kui-dropdown>
  </kui-field>
</div>

Use provider defaults for app-wide combobox behavior. Local inputs still win.

app.config.ts.ts
import { kuiProvideComboboxOptions } from '@kikita-labs/ui';

export const appConfig: ApplicationConfig = {
  providers: [
    kuiProvideComboboxOptions({
      clearable: true,
    }),
  ],
};

Inputs and outputs verified against @kikita-labs/ui v1.6.1 public typings.

NameTypeDefaultDescription
valueT | string | nullnullSelected value. Bound by [formField] or [(value)].
querystring''Current search text shown in the native input.
searchoutput: string-Emitted on every native input edit. Use for local filtering or remote requests.
kuiLabelFn(item: T) => stringString()Maps a selected object value to display text. Required when T is not a primitive.
placeholderstring''Native input placeholder shown when no value is selected.
mode'filter' | 'free' | 'async''filter'filter clears the value while editing until a kuiOption is selected. free stores typed text as the value. async documents that filtering happens outside the directive.
clearableboolean | undefinedtrueShows a clear affordance. Falls back to KUI_COMBOBOX_OPTIONS, then KUI_FIELD_OPTIONS, then true.
loadingbooleanfalseShows a suffix loader. Loading row content inside kui-dropdown is projected by the consumer.
disabledbooleanfalseDisables the native input. Set by [formField] or directly.
readonlybooleanfalseKeeps the value readable but prevents editing and opening the dropdown.
invalidbooleanfalseApplies ARIA invalid state. kui-field also contributes invalid state from validation.
errorsreadonly ValidationError[][]Validation errors set by [formField], consumed by kui-field for automatic error text.
touchedbooleanfalseTouched state set by [formField] or Signal Forms.
touchoutput: void-Emitted when an opened dropdown closes; marks the control touched for Signal Forms.
idstring | undefinedundefinedExplicit id override. Inside kui-field, the field id is used when omitted.
kuiOptiondirective-Marks a projected option inside kui-dropdown. Provides role="option", aria-selected, disabled state, and keyboard navigation.
kuiComboboxHighlightpipe: (label: string, query: string | null | undefined) => readonly { text: string; match: boolean }[]-Splits an option label into plain and matched segments for highlighting the current query.
kuiProvideComboboxOptions(opts: KuiComboboxOptions) => Provider-Registers app-wide combobox defaults, such as clearable, via KUI_COMBOBOX_OPTIONS.
KUI_COMBOBOX_OPTIONSInjectionToken<KuiComboboxOptions>-Injection token backing kuiProvideComboboxOptions. Read by the directive for clearable fallback.
--kui-combobox-affordance-sizeCSS custom property-Size of the suffix clear/chevron affordance controls.
--kui-combobox-suffix-gapCSS custom property-Gap between suffix affordances (clear button, chevron, loader).
--kui-combobox-loader-sizeCSS custom property-Diameter of the suffix loading spinner.
--kui-combobox-loader-border-widthCSS custom property-Border width of the suffix loading spinner.
--kui-combobox-loader-durationCSS custom property-Animation duration of the suffix loading spinner.
--kui-combobox-highlight-bgCSS custom property-Background color of matched text inside kui-combobox-highlight.
--kui-combobox-highlight-textCSS custom property-Text color of matched text inside kui-combobox-highlight.
--kui-combobox-highlight-radiusCSS custom property-Corner radius of the matched-text highlight mark.

Combobox keeps native input semantics and composite listbox behavior on the dropdown.

  • The host remains a native <input>.
  • The input uses role="combobox", aria-haspopup="listbox", aria-expanded, and aria-controls.
  • kui-field provides label, hint, error, required marker, aria-describedby, and inherited invalid state.
  • kui-dropdown renders the popup listbox through Angular CDK overlay.
  • kuiOption provides role="option", aria-selected, disabled state, click selection, and keyboard navigation.
  • Arrow keys open the dropdown and move focus to the first or last enabled option.
  • Escape closes the dropdown.
  • Multiple chip-backed selection is intentionally not part of Combobox; use Select for fixed choices without typing.