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

combobox.ts
import {
  KuiCombobox,
  KuiComboboxHighlightPipe,
  KuiDropdown,
  KuiField,
  KuiOption,
  provideKuiDefaults,
} 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>

Set defaults.combobox once for the application or for a subtree. Each option resolves as local input > defaults.combobox.<option> > built-in default.

import { provideKikitaUi } from '@kikita-labs/ui';

export const appConfig: ApplicationConfig = {
  providers: [
    provideKikitaUi({
      defaults: {
        combobox: {
          /* options below */
        },
      },
    }),
  ],
};

clearable resolves as local input > defaults.combobox > defaults.field > true. The chevron and clear icons take precedence over defaults.icons.pickerChevron and defaults.icons.clear.

NameTypeDefaultDescription
clearableboolean — When true, field controls with clear affordances show a clear button by default.
chevronIconKuiIconGlyph — Icon of the options toggle. Takes precedence over defaults.icons.pickerChevron. See Structural Icons.
clearIconKuiIconGlyph — Icon of the clear button. Takes precedence over defaults.icons.clear. See Structural Icons.

Set any of these on the component or an ancestor to restyle one part. Each token is optional: when it is not set, the part uses the role or scale token in the default column.

NameTypeDefaultDescription
--kui-combobox-chevron-color-expandedCSS custom property--kui-color-primary-textChevron color, expanded
--kui-combobox-loader-colorCSS custom property--kui-color-primary-indicatorLoader color
--kui-combobox-empty-colorCSS custom property--kui-color-text-secondaryEmpty color
--kui-combobox-loader-track-colorCSS custom property--kui-color-borderLoader track color
NameTypeDefaultDescription
--kui-combobox-loader-radiusCSS custom property--kui-radius-fullLoader corner radius
--kui-combobox-match-label-font-sizeCSS custom property--kui-text-sm-sizeMatch label font size
--kui-combobox-empty-padding-blockCSS custom property--kui-space-4Empty padding, block
--kui-combobox-empty-padding-inlineCSS custom property--kui-space-3Empty padding, inline
--kui-combobox-empty-font-sizeCSS custom property--kui-text-sm-sizeEmpty font size
--kui-combobox-loading-row-gapCSS custom property--kui-space-2Loading row gap
--kui-combobox-padding-inline-endCSS custom property34pxInput end padding that clears the chevron
--kui-combobox-padding-inline-end-clearableCSS custom property56pxInput end padding that clears the clear button and the chevron

Every text the component owns (accessible names, visible words, announcements) is a typed message with an English default. Override it for the application with provideKikitaUi({ messages }), or for a subtree with provideKuiMessages.

NameTypeDefaultDescription
openOptionsstring — Name of the open button. Default Open options.
closeOptionsstring — Name of the close button. Default Close options.

Inputs and outputs verified against @kikita-labs/ui v2.0.0 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 defaults.combobox, then defaults.field, 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.
provideKuiDefaults({ combobox })(defaults: KuiComponentDefaults) => Provider-Registers combobox defaults, such as clearable, for a subtree. Use provideKikitaUi({ defaults }) for the whole application.
KuiComboboxOptionsinterface-Shape of the defaults.combobox key, read by the directive for the 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 and returns focus to the control.
  • Choosing an option inside the panel returns focus to the input. With Angular Signal Forms the field gates the required error and aria-invalid until the control is touched, and selecting a valid option clears that state.
  • Multiple chip-backed selection is intentionally not part of Combobox; use Select for fixed choices without typing.

Consumer-visible changes of @kikita-labs/ui 2.0 for Combobox.

  • The classes are KuiCombobox, KuiDropdown, KuiField and KuiOption (they had a Directive or Component suffix); ng update @kikita-labs/ui renames the imports.
  • KUI_COMBOBOX_OPTIONS is removed and kuiProvideComboboxOptions is deprecated until 3.0: set defaults.combobox through provideKuiDefaults or provideKikitaUi({ defaults }).
  • The control no longer pulls the whole field and dropdown into your bundle, the panel returns focus to the control after a selection, and the open and close button names come from the combobox messages.