Form
Select
Usage#
import { WDS_SELECT } from '@wds/angular/select';Examples#
Default#
wds-select-group with a wds-select-label groups the options under a heading.
import { Component } from '@angular/core';
import { WdsSelectComponent, WdsSelectGroupComponent, WdsSelectItemComponent, WdsSelectLabelComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-default-demo',
imports: [WdsSelectComponent, WdsSelectGroupComponent, WdsSelectLabelComponent, WdsSelectItemComponent],
template: `
<wds-select class="w-45" placeholder="Select a fruit" aria-label="Fruit">
<wds-select-group>
<wds-select-label>Fruits</wds-select-label>
<wds-select-item value="apple">Apple</wds-select-item>
<wds-select-item value="banana">Banana</wds-select-item>
<wds-select-item value="blueberry">Blueberry</wds-select-item>
<wds-select-item value="grapes">Grapes</wds-select-item>
<wds-select-item value="pineapple">Pineapple</wds-select-item>
</wds-select-group>
</wds-select>
`,
})
export class SelectDefaultDemo {}Scrollable#
wds-select-separator draws a line between groups. Typing letters jumps to the matching option.
import { Component } from '@angular/core';
import { WdsSelectComponent, WdsSelectGroupComponent, WdsSelectItemComponent, WdsSelectLabelComponent, WdsSelectSeparatorComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-scrollable-demo',
imports: [WdsSelectComponent, WdsSelectGroupComponent, WdsSelectLabelComponent, WdsSelectItemComponent, WdsSelectSeparatorComponent],
template: `
<wds-select class="w-70" placeholder="Select a timezone" aria-label="Timezone">
<wds-select-group>
<wds-select-label>North America</wds-select-label>
<wds-select-item value="est">Eastern Standard Time (EST)</wds-select-item>
<wds-select-item value="cst">Central Standard Time (CST)</wds-select-item>
<wds-select-item value="mst">Mountain Standard Time (MST)</wds-select-item>
<wds-select-item value="pst">Pacific Standard Time (PST)</wds-select-item>
<wds-select-item value="akst">Alaska Standard Time (AKST)</wds-select-item>
<wds-select-item value="hst">Hawaii Standard Time (HST)</wds-select-item>
</wds-select-group>
<wds-select-separator></wds-select-separator>
<wds-select-group>
<wds-select-label>Europe & Africa</wds-select-label>
<wds-select-item value="gmt">Greenwich Mean Time (GMT)</wds-select-item>
<wds-select-item value="cet">Central European Time (CET)</wds-select-item>
<wds-select-item value="eet">Eastern European Time (EET)</wds-select-item>
<wds-select-item value="west">Western European Summer Time (WEST)</wds-select-item>
<wds-select-item value="cat">Central Africa Time (CAT)</wds-select-item>
<wds-select-item value="eat">East Africa Time (EAT)</wds-select-item>
</wds-select-group>
<wds-select-separator></wds-select-separator>
<wds-select-group>
<wds-select-label>Asia</wds-select-label>
<wds-select-item value="msk">Moscow Time (MSK)</wds-select-item>
<wds-select-item value="ist">India Standard Time (IST)</wds-select-item>
<wds-select-item value="cst_china">China Standard Time (CST)</wds-select-item>
<wds-select-item value="jst">Japan Standard Time (JST)</wds-select-item>
<wds-select-item value="kst">Korea Standard Time (KST)</wds-select-item>
</wds-select-group>
</wds-select>
`,
})
export class SelectScrollableDemo {}Multiple#
multiple keeps the list open and shows the selected options as badges. The value is values (an array); wds-change reports it in detail.values. In HTML the selected attribute marks the options selected at first.
[ "angular", "lit" ]
import { JsonPipe } from '@angular/common';
import { Component, signal } from '@angular/core';
import { FormField, form } from '@angular/forms/signals';
import { WdsSelectMultipleField } from '@wds/angular/forms';
import { WDS_SELECT } from '@wds/angular/select';
@Component({
selector: 'app-select-multiple-demo',
imports: [...WDS_SELECT, FormField, JsonPipe, WdsSelectMultipleField],
template: `
<div class="flex w-70 max-w-full flex-col gap-3">
<wds-select multiple placeholder="Select frameworks" aria-label="Frameworks" [formField]="stackForm.frameworks">
<wds-select-item value="angular">Angular</wds-select-item>
<wds-select-item value="react">React</wds-select-item>
<wds-select-item value="vue">Vue</wds-select-item>
<wds-select-item value="svelte">Svelte</wds-select-item>
<wds-select-item value="lit">Lit</wds-select-item>
</wds-select>
<p class="m-0 font-mono text-sm text-muted-foreground">{{ stack().frameworks | json }}</p>
</div>
`,
})
export class SelectMultipleDemo {
protected readonly stack = signal({ frameworks: ['angular', 'lit'] });
protected readonly stackForm = form(this.stack);
}Size#
import { Component } from '@angular/core';
import { WdsSelectComponent, WdsSelectItemComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-size-demo',
imports: [WdsSelectComponent, WdsSelectItemComponent],
template: `
<div class="flex flex-wrap items-center gap-3">
<wds-select class="w-45" size="sm" placeholder="Small" aria-label="Theme, small">
<wds-select-item value="light">Light</wds-select-item>
<wds-select-item value="dark">Dark</wds-select-item>
<wds-select-item value="system">System</wds-select-item>
</wds-select>
<wds-select class="w-45" placeholder="Default" aria-label="Theme, default">
<wds-select-item value="light">Light</wds-select-item>
<wds-select-item value="dark">Dark</wds-select-item>
<wds-select-item value="system">System</wds-select-item>
</wds-select>
<wds-select class="w-45" size="lg" placeholder="Large" aria-label="Theme, large">
<wds-select-item value="light">Light</wds-select-item>
<wds-select-item value="dark">Dark</wds-select-item>
<wds-select-item value="system">System</wds-select-item>
</wds-select>
</div>
`,
})
export class SelectSizeDemo {}Icons#
label).
import { Component } from '@angular/core';
import { WdsSelectComponent, WdsSelectItemComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-icons-demo',
imports: [WdsSelectComponent, WdsSelectItemComponent],
template: `
<wds-select class="w-45" value="card" aria-label="Payment method">
<wds-select-item value="card">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="20" height="14" x="2" y="5" rx="2" /><path d="M2 10h20" /></svg>
Card
</wds-select-item>
<wds-select-item value="transfer">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 3 4 4-4 4" /><path d="M20 7H4" /><path d="m8 21-4-4 4-4" /><path d="M4 17h16" /></svg>
Bank transfer
</wds-select-item>
<wds-select-item value="cash">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="20" height="12" x="2" y="6" rx="2" /><circle cx="12" cy="12" r="2" /></svg>
Cash
</wds-select-item>
</wds-select>
`,
})
export class SelectIconsDemo {}Disabled#
disabled on the select disables the field; on an option it skips the option.
import { Component } from '@angular/core';
import { WdsSelectComponent, WdsSelectItemComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-disabled-demo',
imports: [WdsSelectComponent, WdsSelectItemComponent],
template: `
<div class="flex flex-wrap items-center gap-3">
<wds-select class="w-45" disabled placeholder="Unavailable" aria-label="Unavailable">
<wds-select-item value="a">A</wds-select-item>
</wds-select>
<wds-select class="w-45" placeholder="Select a plan" aria-label="Plan">
<wds-select-item value="free">Free</wds-select-item>
<wds-select-item value="pro">Pro</wds-select-item>
<wds-select-item value="enterprise" disabled>Enterprise (soon)</wds-select-item>
</wds-select>
</div>
`,
})
export class SelectDisabledDemo {}Invalid#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldErrorComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsSelectComponent, WdsSelectItemComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-invalid-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldErrorComponent, WdsSelectComponent, WdsSelectItemComponent],
template: `
<wds-field class="w-full max-w-xs" invalid>
<wds-field-label>Country</wds-field-label>
<wds-select class="w-full" placeholder="Select a country">
<wds-select-item value="us">United States</wds-select-item>
<wds-select-item value="uk">United Kingdom</wds-select-item>
<wds-select-item value="de">Germany</wds-select-item>
</wds-select>
<wds-field-error>Select a shipping country.</wds-field-error>
</wds-field>
`,
})
export class SelectInvalidDemo {}Field#
wds-field the label and the description become the accessible name and description of the select.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsSelectComponent, WdsSelectItemComponent } from '@wds/angular/select';
@Component({
selector: 'app-select-field-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsSelectComponent, WdsSelectItemComponent],
template: `
<wds-field class="w-full max-w-xs">
<wds-field-label>Department</wds-field-label>
<wds-select class="w-full" placeholder="Select a department" value="engineering">
<wds-select-item value="engineering">Engineering</wds-select-item>
<wds-select-item value="design">Design</wds-select-item>
<wds-select-item value="marketing">Marketing</wds-select-item>
<wds-select-item value="sales">Sales</wds-select-item>
</wds-select>
<wds-field-description>We use it to assign permissions.</wds-field-description>
</wds-field>
`,
})
export class SelectFieldDemo {}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsSelectField connects a single select to [formField] (a string), and WdsSelectMultipleField a multiple select (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_FIELD } from '@wds/angular/field';
import { WdsSelectField, WdsSelectMultipleField } from '@wds/angular/forms';
import { WDS_SELECT } from '@wds/angular/select';
@Component({
selector: 'app-select-signal-forms-demo',
imports: [...WDS_FIELD, ...WDS_SELECT, FormField, FormRoot, WdsButtonComponent, WdsSelectField, WdsSelectMultipleField],
template: `
<form class="flex w-sm max-w-full flex-col gap-4" [formRoot]="preferencesForm">
<wds-field [invalid]="showLanguageError()">
<wds-field-label>Language</wds-field-label>
<wds-select class="w-full" placeholder="Select a language" [formField]="preferencesForm.language">
<wds-select-item value="en">English</wds-select-item>
<wds-select-item value="de">German</wds-select-item>
<wds-select-item value="pl">Polish</wds-select-item>
</wds-select>
<wds-field-error [errors]="showLanguageError() ? preferencesForm.language().errors() : []" />
</wds-field>
<wds-field>
<wds-field-label>Notification channels</wds-field-label>
<wds-select class="w-full" multiple placeholder="Select channels" [formField]="preferencesForm.channels">
<wds-select-item value="email">Email</wds-select-item>
<wds-select-item value="sms">SMS</wds-select-item>
<wds-select-item value="push">Push</wds-select-item>
</wds-select>
</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 SelectSignalFormsDemo {
protected readonly preferences = signal({ language: '', channels: ['email'] });
protected readonly submitted = signal('—');
protected readonly preferencesForm = form(
this.preferences,
(path) => {
required(path.language, { message: 'Select a language.' });
},
{
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 showLanguageError = computed(
() => this.preferencesForm.language().touched() && this.preferencesForm.language().invalid()
);
}Template-driven#
WdsSelectValueAccessor makes ngModel work on wds-select, for single (string) and multiple (string[]) selects.
import { Component, signal } from '@angular/core';
import { FormsModule, type NgForm } from '@angular/forms';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsSelectValueAccessor } from '@wds/angular/forms';
import { WDS_SELECT } from '@wds/angular/select';
@Component({
selector: 'app-select-template-driven-demo',
imports: [...WDS_FIELD, ...WDS_SELECT, FormsModule, WdsButtonComponent, WdsSelectValueAccessor],
template: `
<form #preferencesForm="ngForm" class="flex w-sm max-w-full flex-col gap-4" (ngSubmit)="save(preferencesForm)">
<wds-field [invalid]="!!language.invalid && !!language.touched">
<wds-field-label>Language</wds-field-label>
<wds-select #language="ngModel" class="w-full" name="language" placeholder="Select a language" [(ngModel)]="preferences.language" required>
<wds-select-item value="en">English</wds-select-item>
<wds-select-item value="de">German</wds-select-item>
<wds-select-item value="pl">Polish</wds-select-item>
</wds-select>
@if (language.invalid && language.touched) {
<wds-field-error>Select a language.</wds-field-error>
}
</wds-field>
<wds-field>
<wds-field-label>Notification channels</wds-field-label>
<!-- The same accessor handles multiple: the value is a string[]. -->
<wds-select class="w-full" multiple name="channels" placeholder="Select channels" [(ngModel)]="preferences.channels">
<wds-select-item value="email">Email</wds-select-item>
<wds-select-item value="sms">SMS</wds-select-item>
<wds-select-item value="push">Push</wds-select-item>
</wds-select>
</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 SelectTemplateDrivenDemo {
protected readonly preferences = { language: '', channels: ['email'] };
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_FIELD } from '@wds/angular/field';
import { WdsSelectValueAccessor } from '@wds/angular/forms';
import { WDS_SELECT } from '@wds/angular/select';
@Component({
selector: 'app-select-reactive-forms-demo',
imports: [...WDS_FIELD, ...WDS_SELECT, ReactiveFormsModule, WdsButtonComponent, WdsSelectValueAccessor],
template: `
<form class="flex w-sm max-w-full flex-col gap-4" [formGroup]="preferencesForm" (ngSubmit)="save()">
<wds-field [invalid]="language.invalid && language.touched">
<wds-field-label>Language</wds-field-label>
<wds-select class="w-full" placeholder="Select a language" formControlName="language">
<wds-select-item value="en">English</wds-select-item>
<wds-select-item value="de">German</wds-select-item>
<wds-select-item value="pl">Polish</wds-select-item>
</wds-select>
@if (language.invalid && language.touched) {
<wds-field-error>Select a language.</wds-field-error>
}
</wds-field>
<wds-field>
<wds-field-label>Notification channels</wds-field-label>
<!-- The same accessor handles multiple: the value is a string[]. -->
<wds-select class="w-full" multiple placeholder="Select channels" formControlName="channels">
<wds-select-item value="email">Email</wds-select-item>
<wds-select-item value="sms">SMS</wds-select-item>
<wds-select-item value="push">Push</wds-select-item>
</wds-select>
</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 SelectReactiveFormsDemo {
protected readonly preferencesForm = new FormGroup({
language: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
channels: new FormControl<string[]>(['email'], { nonNullable: true }),
});
protected readonly language = this.preferencesForm.controls.language;
protected readonly submitted = signal('—');
protected save(): void {
if (this.preferencesForm.invalid) {
this.preferencesForm.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(this.preferencesForm.getRawValue()));
}
}API References#
wds-select#
aria-activedescendant points to the active option. The list opens in the top layer (Popover API).
In a form it works like <select>: name, required, reset to the options with the selected attribute.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| value | string | '' | |
| multiple | boolean | false | |
| placeholder | string | '' | |
| name | string | '' | |
| disabled | boolean | false | |
| required | boolean | false | |
| invalid | boolean | false | aria-invalid). |
| size | WdsSelectSize | 'default' | |
| max-label-count.maxLabelCount | number | 2 | multiple mode: how many selected options to show as badges, the rest as "+N". 0 = all. |
| open | boolean | false |
Properties
| Property | Description |
|---|---|
| valuesstring[] | multiple mode). |
Events
| Event | Description |
|---|---|
| wds-open-changeCustomEvent<WdsSelectOpenChangeDetail> | |
| wds-changeCustomEvent<WdsSelectChangeDetail> |
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-select-item, wds-select-group, wds-select-separator. |
CSS parts (::part)
| Part | Description |
|---|---|
| trigger | |
| value | |
| content |
wds-select-item#
wds-select (role="option"). The selected attribute marks an option selected by default
(like <option selected>), also for form reset.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| 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 |
|---|---|
| base | |
| indicator |
wds-select-group#
role="group"), named by its first label (wds-select-label / wds-combobox-label).
Slots
| Slot | Description |
|---|---|
| (default) | wds-select-label and wds-select-item. |
wds-select-label#
wds-select-group.
Slots
| Slot | Description |
|---|---|
| (default) |
CSS parts (::part)
| Part | Description |
|---|---|
| base |
wds-select-separator#
CSS parts (::part)
| Part | Description |
|---|---|
| base |
Accessibility#
Screen reader#
wds-select follows the WAI-ARIA select-only combobox pattern: the field has role="combobox" with aria-expanded and aria-haspopup="listbox", and the list role="listbox" (with aria-multiselectable in multiple mode). Focus stays on the field; aria-activedescendant points to the active option.
wds-field-label in wds-field, or aria-label on the element. The placeholder is not a name.
role="option" with aria-selected. wds-select-group has role="group" named by its wds-select-label; wds-select-separator and the check marks are hidden from screen readers.
invalid, required and disabled set aria-invalid, aria-required and aria-disabled on the field. In a form the value is submitted under name, and a reset restores the options with selected.
Keyboard support#
| Key | Function |
|---|---|