Form
Combobox
Usage#
import { WDS_COMBOBOX } from '@wds/angular/combobox';Examples#
Default#
wds-change with its value; selecting the selected option again clears the value. Set search-placeholder and empty-text: their defaults are in Polish.
import { Component } from '@angular/core';
import { WdsComboboxComponent, WdsComboboxItemComponent } from '@wds/angular/combobox';
@Component({
selector: 'app-combobox-default-demo',
imports: [WdsComboboxComponent, WdsComboboxItemComponent],
template: `
<wds-combobox class="w-50" aria-label="Framework" placeholder="Select framework…" search-placeholder="Search framework…" empty-text="No framework found.">
<wds-combobox-item value="angular">Angular</wds-combobox-item>
<wds-combobox-item value="react">React</wds-combobox-item>
<wds-combobox-item value="vue">Vue</wds-combobox-item>
<wds-combobox-item value="svelte">Svelte</wds-combobox-item>
<wds-combobox-item value="astro">Astro</wds-combobox-item>
</wds-combobox>
`,
})
export class ComboboxDefaultDemo {}Groups#
wds-combobox-group with a wds-combobox-label groups the options. The search ignores case and diacritics: type “lodz” or “krakow”. Groups without a match hide.
import { Component } from '@angular/core';
import { WdsComboboxComponent, WdsComboboxGroupComponent, WdsComboboxItemComponent, WdsComboboxLabelComponent, WdsComboboxSeparatorComponent } from '@wds/angular/combobox';
@Component({
selector: 'app-combobox-groups-demo',
imports: [WdsComboboxComponent, WdsComboboxGroupComponent, WdsComboboxLabelComponent, WdsComboboxItemComponent, WdsComboboxSeparatorComponent],
template: `
<wds-combobox class="w-65" aria-label="City" placeholder="Select city…" search-placeholder="Search city…" empty-text="No city found.">
<wds-combobox-group>
<wds-combobox-label>Poland</wds-combobox-label>
<wds-combobox-item value="waw">Warsaw</wds-combobox-item>
<wds-combobox-item value="krk">Kraków</wds-combobox-item>
<wds-combobox-item value="lodz">Łódź</wds-combobox-item>
<wds-combobox-item value="gda">Gdańsk</wds-combobox-item>
</wds-combobox-group>
<wds-combobox-separator></wds-combobox-separator>
<wds-combobox-group>
<wds-combobox-label>Germany</wds-combobox-label>
<wds-combobox-item value="ber">Berlin</wds-combobox-item>
<wds-combobox-item value="muc">Munich</wds-combobox-item>
<wds-combobox-item value="ham">Hamburg</wds-combobox-item>
</wds-combobox-group>
<wds-combobox-separator></wds-combobox-separator>
<wds-combobox-group>
<wds-combobox-label>Czechia</wds-combobox-label>
<wds-combobox-item value="prg">Prague</wds-combobox-item>
<wds-combobox-item value="brn">Brno</wds-combobox-item>
</wds-combobox-group>
</wds-combobox>
`,
})
export class ComboboxGroupsDemo {}Keywords#
keywords adds words that match without being shown: type “dollar” or “£”.
import { Component } from '@angular/core';
import { WdsComboboxComponent, WdsComboboxItemComponent } from '@wds/angular/combobox';
@Component({
selector: 'app-combobox-keywords-demo',
imports: [WdsComboboxComponent, WdsComboboxItemComponent],
template: `
<wds-combobox class="w-65" aria-label="Currency" placeholder="Select currency…" search-placeholder="Search currency…" empty-text="No currency found.">
<wds-combobox-item value="PLN" keywords="zloty zł">Polish złoty (PLN)</wds-combobox-item>
<wds-combobox-item value="EUR" keywords="€">Euro (EUR)</wds-combobox-item>
<wds-combobox-item value="USD" keywords="dollar $ usa">US dollar (USD)</wds-combobox-item>
<wds-combobox-item value="GBP" keywords="pound sterling £">British pound (GBP)</wds-combobox-item>
<wds-combobox-item value="CHF" keywords="franc">Swiss franc (CHF)</wds-combobox-item>
</wds-combobox>
`,
})
export class ComboboxKeywordsDemo {}Disabled#
disabled on the combobox disables the field; on an option it skips the option.
import { Component } from '@angular/core';
import { WdsComboboxComponent, WdsComboboxItemComponent } from '@wds/angular/combobox';
@Component({
selector: 'app-combobox-disabled-demo',
imports: [WdsComboboxComponent, WdsComboboxItemComponent],
template: `
<div class="flex flex-wrap items-center justify-center gap-3">
<wds-combobox class="w-50" aria-label="Framework" placeholder="Select framework…" disabled>
<wds-combobox-item value="angular">Angular</wds-combobox-item>
</wds-combobox>
<wds-combobox class="w-50" aria-label="Status" placeholder="Select status…" search-placeholder="Search status…" empty-text="No status found.">
<wds-combobox-item value="todo">Todo</wds-combobox-item>
<wds-combobox-item value="in-progress">In progress</wds-combobox-item>
<wds-combobox-item value="done">Done</wds-combobox-item>
<wds-combobox-item value="archived" disabled>Archived</wds-combobox-item>
</wds-combobox>
</div>
`,
})
export class ComboboxDisabledDemo {}Invalid#
import { Component } from '@angular/core';
import { WdsComboboxComponent, WdsComboboxItemComponent } from '@wds/angular/combobox';
@Component({
selector: 'app-combobox-invalid-demo',
imports: [WdsComboboxComponent, WdsComboboxItemComponent],
template: `
<wds-combobox class="w-50" aria-label="Framework" placeholder="Select framework…" search-placeholder="Search framework…" empty-text="No framework found." invalid>
<wds-combobox-item value="angular">Angular</wds-combobox-item>
<wds-combobox-item value="react">React</wds-combobox-item>
<wds-combobox-item value="vue">Vue</wds-combobox-item>
</wds-combobox>
`,
})
export class ComboboxInvalidDemo {}Field#
wds-field the label and the description become the accessible name and description of the combobox.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsComboboxComponent, WdsComboboxItemComponent } from '@wds/angular/combobox';
@Component({
selector: 'app-combobox-field-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsComboboxComponent, WdsComboboxItemComponent],
template: `
<wds-field class="w-full max-w-xs">
<wds-field-label>Country</wds-field-label>
<wds-combobox class="w-full" placeholder="Select country…" search-placeholder="Search country…" empty-text="No country found.">
<wds-combobox-item value="pl">Poland</wds-combobox-item>
<wds-combobox-item value="de">Germany</wds-combobox-item>
<wds-combobox-item value="cz">Czechia</wds-combobox-item>
<wds-combobox-item value="sk">Slovakia</wds-combobox-item>
</wds-combobox>
<wds-field-description>We ship to these countries only.</wds-field-description>
</wds-field>
`,
})
export class ComboboxFieldDemo {}Server search#
filter="none" the combobox does not filter by itself: it fires wds-search and the app replaces the options (here a simulated 400 ms request). The empty slot shows the loading state and the message when nothing matches.
import { Component, signal } from '@angular/core';
import { WDS_COMBOBOX } from '@wds/angular/combobox';
import { WdsSpinnerComponent } from '@wds/angular/spinner';
const CITIES = [
'Amsterdam', 'Athens', 'Barcelona', 'Berlin', 'Brussels', 'Budapest', 'Copenhagen', 'Dublin', 'Helsinki', 'Kraków',
'Lisbon', 'London', 'Madrid', 'Milan', 'Munich', 'Oslo', 'Paris', 'Prague', 'Rome', 'Stockholm', 'Vienna', 'Warsaw',
];
/** Simulates a request to a server. */
const fetchCities = (query: string) =>
new Promise<string[]>((resolve) =>
setTimeout(() => resolve(CITIES.filter((c) => c.toLowerCase().includes(query.trim().toLowerCase())).slice(0, 8)), 400)
);
@Component({
selector: 'app-combobox-server-search-demo',
imports: [...WDS_COMBOBOX, WdsSpinnerComponent],
template: `
<wds-combobox
class="w-65"
aria-label="City"
filter="none"
placeholder="Select city…"
search-placeholder="Type a city name…"
(wdsSearch)="search($event.query)"
>
@for (city of results(); track city) {
<wds-combobox-item [value]="city">{{ city }}</wds-combobox-item>
}
<span slot="empty">
@if (loading()) {
<span class="inline-flex items-center gap-2 text-muted-foreground"><wds-spinner decorative size="sm" />Searching…</span>
} @else {
No city matches “{{ query() }}”.
}
</span>
</wds-combobox>
`,
})
export class ComboboxServerSearchDemo {
protected readonly query = signal('');
protected readonly loading = signal(false);
protected readonly results = signal(CITIES.slice(0, 8));
private request = 0;
protected async search(query: string): Promise<void> {
const id = ++this.request;
this.query.set(query);
this.loading.set(true);
this.results.set([]);
const cities = await fetchCities(query);
// Only the latest request updates the list.
if (id !== this.request) return;
this.results.set(cities);
this.loading.set(false);
}
}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsSelectField connects wds-combobox to [formField]; the value is a string.
import { Component, computed, signal } from '@angular/core';
import { FormField, FormRoot, form, required } from '@angular/forms/signals';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_COMBOBOX } from '@wds/angular/combobox';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSelectField } from '@wds/angular/forms';
@Component({
selector: 'app-combobox-signal-forms-demo',
imports: [...WDS_COMBOBOX, ...WDS_FIELD, FormField, FormRoot, WdsButtonComponent, WdsSelectField],
template: `
<form class="flex w-sm max-w-full flex-col gap-4" [formRoot]="taskForm">
<wds-field [invalid]="showAssigneeError()">
<wds-field-label>Assignee</wds-field-label>
<wds-combobox class="w-full" placeholder="Select person…" search-placeholder="Search person…" empty-text="No person found." [formField]="taskForm.assignee">
<wds-combobox-item value="anna">Anna Smith</wds-combobox-item>
<wds-combobox-item value="john">John Carter</wds-combobox-item>
<wds-combobox-item value="maria">Maria Garcia</wds-combobox-item>
<wds-combobox-item value="wei">Wei Chen</wds-combobox-item>
</wds-combobox>
<wds-field-error [errors]="showAssigneeError() ? taskForm.assignee().errors() : []" />
</wds-field>
<wds-button type="submit" class="self-start">Submit</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class ComboboxSignalFormsDemo {
protected readonly task = signal({ assignee: '' });
protected readonly submitted = signal('—');
protected readonly taskForm = form(
this.task,
(path) => {
required(path.assignee, { message: 'Select a person.' });
},
{
submission: {
action: async (form) => {
this.submitted.set(JSON.stringify(form().value()));
},
},
}
);
/** The error appears after the user leaves the field or submits the form. */
protected readonly showAssigneeError = computed(() => this.taskForm.assignee().touched() && this.taskForm.assignee().invalid());
}Template-driven#
WdsSelectValueAccessor makes ngModel work on wds-combobox.
import { Component, signal } from '@angular/core';
import { FormsModule, type NgForm } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_COMBOBOX } from '@wds/angular/combobox';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSelectValueAccessor } from '@wds/angular/forms';
@Component({
selector: 'app-combobox-template-driven-demo',
imports: [...WDS_COMBOBOX, ...WDS_FIELD, FormsModule, WdsButtonComponent, WdsSelectValueAccessor],
template: `
<form #taskForm="ngForm" class="flex w-sm max-w-full flex-col gap-4" (ngSubmit)="save(taskForm)">
<wds-field [invalid]="!!assignee.invalid && !!assignee.touched">
<wds-field-label>Assignee</wds-field-label>
<wds-combobox
#assignee="ngModel"
class="w-full"
name="assignee"
placeholder="Select person…"
search-placeholder="Search person…"
empty-text="No person found."
[(ngModel)]="task.assignee"
required
>
<wds-combobox-item value="anna">Anna Smith</wds-combobox-item>
<wds-combobox-item value="john">John Carter</wds-combobox-item>
<wds-combobox-item value="maria">Maria Garcia</wds-combobox-item>
<wds-combobox-item value="wei">Wei Chen</wds-combobox-item>
</wds-combobox>
@if (assignee.invalid && assignee.touched) {
<wds-field-error>Select a person.</wds-field-error>
}
</wds-field>
<wds-button type="submit" class="self-start">Submit</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class ComboboxTemplateDrivenDemo {
protected readonly task = { assignee: '' };
protected readonly submitted = signal('—');
protected save(form: NgForm): void {
if (form.invalid) {
form.form.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(form.value));
}
}Reactive forms#
WdsSelectValueAccessor works with formControl and formControlName.
import { Component, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_COMBOBOX } from '@wds/angular/combobox';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSelectValueAccessor } from '@wds/angular/forms';
@Component({
selector: 'app-combobox-reactive-forms-demo',
imports: [...WDS_COMBOBOX, ...WDS_FIELD, ReactiveFormsModule, WdsButtonComponent, WdsSelectValueAccessor],
template: `
<form class="flex w-sm max-w-full flex-col gap-4" [formGroup]="taskForm" (ngSubmit)="save()">
<wds-field [invalid]="assignee.invalid && assignee.touched">
<wds-field-label>Assignee</wds-field-label>
<wds-combobox class="w-full" placeholder="Select person…" search-placeholder="Search person…" empty-text="No person found." formControlName="assignee">
<wds-combobox-item value="anna">Anna Smith</wds-combobox-item>
<wds-combobox-item value="john">John Carter</wds-combobox-item>
<wds-combobox-item value="maria">Maria Garcia</wds-combobox-item>
<wds-combobox-item value="wei">Wei Chen</wds-combobox-item>
</wds-combobox>
@if (assignee.invalid && assignee.touched) {
<wds-field-error>Select a person.</wds-field-error>
}
</wds-field>
<wds-button type="submit" class="self-start">Submit</wds-button>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class ComboboxReactiveFormsDemo {
protected readonly taskForm = new FormGroup({
assignee: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
});
protected readonly assignee = this.taskForm.controls.assignee;
protected readonly submitted = signal('—');
protected save(): void {
if (this.taskForm.invalid) {
this.taskForm.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(this.taskForm.getRawValue()));
}
}API References#
wds-combobox#
role="combobox") and aria-activedescendant
points to the active option. Selecting the selected option again clears the value.
Works in a form like <select>: name, required, and a reset restores the option with selected.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| value | string | '' | |
| placeholder | string | '' | |
| search-placeholder.searchPlaceholder | string | 'Szukaj…' | |
| empty-text.emptyText | string | 'Nie znaleziono.' | empty slot takes precedence. |
| filter | WdsComboboxFilter | 'auto' | auto filters the options by their text. none leaves filtering to the app
(e.g. results from a server after wds-search). |
| name | string | '' | |
| disabled | boolean | false | |
| required | boolean | false | valueMissing when empty). |
| invalid | boolean | false | aria-invalid). |
| open | boolean | false |
Events
| Event | Description |
|---|---|
| wds-open-changeCustomEvent<WdsComboboxOpenChangeDetail> | |
| wds-changeCustomEvent<WdsComboboxChangeDetail> | |
| wds-searchCustomEvent<WdsComboboxSearchDetail> |
Methods
| Method | Description |
|---|---|
| show() | |
| close() | |
| formResetCallback() | |
| formDisabledCallback(disabled: boolean) | |
| checkValidity() | true when the value is valid; otherwise fires invalid on the element. |
| reportValidity() | checkValidity(), and shows the browser validation bubble when the value is invalid. |
| setCustomValidity(message: string) | input.setCustomValidity(). |
Slots
| Slot | Description |
|---|---|
| (default) | wds-combobox-item, wds-combobox-group, wds-combobox-separator. |
| empty | empty-text), e.g. a spinner while loading. |
CSS parts (::part)
| Part | Description |
|---|---|
| trigger | |
| value | |
| content | |
| input | |
| list | |
| empty |
CSS custom properties
| Property | Description |
|---|---|
| --wds-combobox-list-height |
wds-combobox-item#
wds-combobox (role="option"). The selected attribute marks the default selection.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| keywords | string | '' | |
| value | string | '' | |
| label | string | undefined | ||
| disabled | boolean | false |
Properties
| Property | Description |
|---|---|
| selectedboolean | wds-select sets it; the default selection is the selected attribute. |
Slots
| Slot | Description |
|---|---|
| (default) | <svg> icon). |
CSS parts (::part)
| Part | Description |
|---|---|
| indicator | |
| base |
wds-combobox-group#
wds-combobox options (role="group") named by its wds-combobox-label.
It hides when none of its options match the search.
Slots
| Slot | Description |
|---|---|
| (default) | wds-combobox-label and wds-combobox-item. |
wds-combobox-label#
wds-combobox-group.
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-combobox-separator#
CSS parts (::part)
| Part | Description |
|---|---|
| base |
Accessibility#
Screen reader#
<button> with role="combobox", aria-haspopup="listbox" and aria-expanded. Give it a name: wds-field-label in wds-field, or aria-label on the element. The placeholder is not a name.
role="combobox", aria-autocomplete="list"). aria-activedescendant points to the active option of the list (role="listbox"); both are named like the field. Without a name the search field is named by search-placeholder.
role="option" with aria-selected. wds-combobox-group has role="group" named by its wds-combobox-label; separators and check marks are hidden from screen readers.
empty-text or the empty slot) is a role="status" region, so it is announced.
invalid and required set aria-invalid and aria-required on the field. In a form the value is submitted under name, and a reset restores the option with selected.
Keyboard support#
| Key | Function |
|---|---|