Combobox
A searchable select with single and multi-select modes for Angular forms.
import { … } from '@inlaylab/ivorie/combobox'Value:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';
@Component({
selector: 'demo-combobox',
imports: [IV_COMBOBOX_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="flex flex-col items-center gap-3">
<iv-combobox [(value)]="fruit" placeholder="Search fruit…" class="w-56">
<iv-combobox-input [showClear]="true" aria-label="Fruit" />
<iv-combobox-content>
<iv-combobox-list>
<iv-combobox-item value="apple" label="Apple">Apple</iv-combobox-item>
<iv-combobox-item value="banana" label="Banana">Banana</iv-combobox-item>
<iv-combobox-item value="orange" label="Orange">Orange</iv-combobox-item>
<iv-combobox-empty>No fruit found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>
<p class="text-muted-foreground text-sm">Value: {{ fruit() }}</p>
</div>
`,
})
export default class ComboboxDemo {
protected readonly fruit = signal<string | string[] | null>(null);
}
Installation
npx ivorie add comboboximport { … } from '@inlaylab/ivorie/combobox'An autocomplete input with a filterable list. It supports single select, multi-select with chips, groups, an empty state, ngModel and reactive forms.
Usage
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';<iv-combobox [(value)]="fruit">
<iv-combobox-input aria-label="Fruit" />
<iv-combobox-content>
<iv-combobox-list>
<iv-combobox-item value="apple" label="Apple">Apple</iv-combobox-item>
<iv-combobox-item value="banana" label="Banana">Banana</iv-combobox-item>
<iv-combobox-empty>No fruit found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>Each item needs label. Search filters on it, so it must match the visible text.
Examples
Multiple with chips
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';
@Component({
selector: 'demo-combobox-multiple',
imports: [IV_COMBOBOX_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-combobox [(value)]="selected" [multiple]="true" class="w-64">
<iv-combobox-chips aria-label="Fruits">
@for (v of selected(); track v) {
<iv-combobox-chip [value]="v" [label]="labelFor(v)" />
}
</iv-combobox-chips>
<iv-combobox-content>
<iv-combobox-list>
@for (f of fruits; track f.value) {
<iv-combobox-item [value]="f.value" [label]="f.label">{{ f.label }}</iv-combobox-item>
}
<iv-combobox-empty>No fruit found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>
`,
})
export default class ComboboxMultipleDemo {
protected readonly fruits = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'orange', label: 'Orange' },
];
protected readonly selected = signal<string[]>(['apple']);
protected labelFor(value: string) {
return this.fruits.find((f) => f.value === value)?.label ?? value;
}
}
Reactive forms
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';
@Component({
selector: 'demo-combobox-reactive-forms',
imports: [ReactiveFormsModule, IV_COMBOBOX_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<form [formGroup]="form" class="flex flex-col items-center gap-3">
<iv-combobox formControlName="country" placeholder="Search country…" class="w-56">
<iv-combobox-input aria-label="Country" />
<iv-combobox-content>
<iv-combobox-list>
@for (c of countries; track c.value) {
<iv-combobox-item [value]="c.value" [label]="c.label">{{ c.label }}</iv-combobox-item>
}
<iv-combobox-empty>No country found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>
<p class="text-muted-foreground text-sm">Value: {{ form.value.country }}</p>
</form>
`,
})
export default class ComboboxReactiveFormsDemo {
protected readonly countries = [
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'jp', label: 'Japan' },
];
protected readonly form = new FormGroup({ country: new FormControl<string | null>('de') });
}
Groups
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';
@Component({
selector: 'demo-combobox-groups',
imports: [IV_COMBOBOX_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-combobox [(value)]="selected" placeholder="Search…" class="w-56">
<iv-combobox-input aria-label="Produce" />
<iv-combobox-content>
<iv-combobox-list>
<iv-combobox-group>
<iv-combobox-label>Fruits</iv-combobox-label>
<iv-combobox-item value="apple" label="Apple">Apple</iv-combobox-item>
<iv-combobox-item value="pear" label="Pear">Pear</iv-combobox-item>
</iv-combobox-group>
<iv-combobox-separator />
<iv-combobox-group>
<iv-combobox-label>Vegetables</iv-combobox-label>
<iv-combobox-item value="carrot" label="Carrot">Carrot</iv-combobox-item>
<iv-combobox-item value="leek" label="Leek">Leek</iv-combobox-item>
</iv-combobox-group>
<iv-combobox-empty>Nothing found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>
`,
})
export default class ComboboxGroupsDemo {
protected readonly selected = signal<string | string[] | null>(null);
}
Without trigger button
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';
@Component({
selector: 'demo-combobox-no-trigger',
imports: [IV_COMBOBOX_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-combobox [(value)]="fruit" placeholder="Type to search…" class="w-56">
<iv-combobox-input [showTrigger]="false" [showClear]="true" aria-label="Fruit" />
<iv-combobox-content>
<iv-combobox-list>
<iv-combobox-item value="apple" label="Apple">Apple</iv-combobox-item>
<iv-combobox-item value="banana" label="Banana">Banana</iv-combobox-item>
<iv-combobox-item value="orange" label="Orange">Orange</iv-combobox-item>
<iv-combobox-empty>No fruit found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>
`,
})
export default class ComboboxNoTriggerDemo {
protected readonly fruit = signal<string | string[] | null>(null);
}
Filter label
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_COMBOBOX_IMPORTS } from '@inlaylab/ivorie/combobox';
@Component({
selector: 'demo-combobox-filter-label',
imports: [IV_COMBOBOX_IMPORTS],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<iv-combobox [(value)]="user" placeholder="All" class="w-64">
<iv-combobox-input label="User" aria-label="User filter" />
<iv-combobox-content>
<iv-combobox-list>
<iv-combobox-item value="ada" label="Ada">Ada</iv-combobox-item>
<iv-combobox-item value="grace" label="Grace">Grace</iv-combobox-item>
<iv-combobox-item value="linus" label="Linus">Linus</iv-combobox-item>
<iv-combobox-empty>No user found.</iv-combobox-empty>
</iv-combobox-list>
</iv-combobox-content>
</iv-combobox>
`,
})
export default class ComboboxFilterLabelDemo {
protected readonly user = signal<string | string[] | null>('grace');
}
API
Every part also accepts class.
iv-combobox
| Name | Type | Default | Description |
|---|---|---|---|
value |
string | string[] | null |
null |
Selected value. A string[] when multiple. Bind with [(value)]. |
placeholder |
string |
'Select…' |
Input placeholder. |
disabled |
boolean |
false |
Disables all interaction. Also disabled when the form control is disabled. |
multiple |
boolean |
false |
Multi-select mode. |
Outputs:
| Name | Payload | Description |
|---|---|---|
valueChange |
string | string[] | null |
Emits when the user selects or removes a value. |
searchChange |
string |
Emits as the search text changes. Use it for server-side filtering. |
searchSubmit |
string |
Enter with the list open and no active item. Emits the trimmed search, then clears it. Not emitted for an empty search or for the selected item's label. |
iv-combobox-input
Search input in a group, with optional trigger and clear buttons. class goes on the container.
| Name | Type | Default | Description |
|---|---|---|---|
showTrigger |
boolean |
true |
Shows the chevron button. |
showClear |
boolean |
false |
Shows a clear button when the search is not empty. |
placeholder |
string | undefined |
root placeholder |
Overrides the root placeholder. |
id |
string | null |
null |
Set on the inner input, so <label for> works. |
aria-label |
string | null |
null |
Set on the inner input. |
label |
string |
'' |
Static prefix before the input, such as User: All. With a value, the input gets data-active. |
iv-combobox-chips
Multi-select container with a built-in text input. Put iv-combobox-chip elements inside. class goes on the container.
| Name | Type | Default | Description |
|---|---|---|---|
id |
string | null |
null |
Set on the inner input, so <label for> works. |
aria-label |
string | null |
null |
Set on the inner input. |
iv-combobox-chip
A selected value with a remove button.
| Name | Type | Default | Description |
|---|---|---|---|
value |
string |
required | Value removed when the chip is dismissed. |
label |
string |
'' |
Text shown in the chip. |
iv-combobox-content
The dropdown panel, shown in a CDK overlay anchored to this element.
| Name | Type | Default | Description |
|---|---|---|---|
align |
'start' | 'end' |
'start' |
Alignment against the anchor. |
side |
'auto' | 'bottom' |
'auto' |
'auto' flips above when there is no room below. |
iv-combobox-list
Scrollable listbox inside the content.
| Name | Type | Default | Description |
|---|---|---|---|
id |
string | undefined |
generated | Id of the listbox. The input and chips point aria-controls at it. |
iv-combobox-item
| Name | Type | Default | Description |
|---|---|---|---|
value |
string |
required | Value set on selection. |
label |
string |
'' |
Text used for search and for the input after selection. |
disabled |
boolean |
false |
Blocks selection. |
iv-combobox-group, iv-combobox-label, iv-combobox-separator, iv-combobox-empty
Layout parts with no inputs besides class. The empty part shows only when no item matches the search.
Accessibility
- The input has
role="combobox",aria-autocomplete="list",aria-expanded,aria-controlsandaria-activedescendant. Focus stays in the input. - The list has
role="listbox"andaria-multiselectablein multiple mode. Items haverole="option",aria-selectedandaria-disabled. ArrowDownandArrowUpopen the list and move the active item (wraps). Disabled and filtered-out items are skipped.Enterwith the list open selects the active item, or submits the search when none is active. With the list closed it does nothing.Escapecloses an open list and is handled, so an enclosing dialog stays open.Tabcloses it.Backspacein an empty chips input removes the last chip.- The trigger button is labelled "Open" or "Close". The clear button is labelled "Clear search". Each chip's remove button is labelled "Remove
Notes
- Filtering is client-side. An item shows when its lower-case
labelcontains the trimmed lower-case search. An empty search shows every item. An item with an emptylabelis hidden once the search is not empty. - For server-side filtering, listen to
searchChangeand update the items. Keep eachlabelmatching what the server returns. - In single mode the input shows the selected item's label. Closing the list restores it.
- Closing marks the form control touched.
- The panel opens below, or above when there is no room, and is not clipped by parent
overflow. - A truncated value in
iv-combobox-inputshows a tooltip on hover when the list is closed. ngModelandformControlNameneedFormsModuleorReactiveFormsModule.