Form
Input
Usage#
import { WdsInputComponent } from '@wds/angular/input';Examples#
Default#
wds-field give the input a name with aria-label.
import { Component } from '@angular/core';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-default-demo',
imports: [WdsInputComponent],
template: `
<wds-input class="w-full max-w-xs" placeholder="Enter text" aria-label="Text"></wds-input>
`,
})
export class InputDefaultDemo {}Field#
wds-field the label and the description become the accessible name and description of the input.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-field-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Username</wds-field-label>
<wds-input placeholder="Enter your username" autocomplete="username"></wds-input>
<wds-field-description>Choose a unique username for your account.</wds-field-description>
</wds-field>
`,
})
export class InputFieldDemo {}Form#
wds-input is a form-associated element: it is submitted under name, validated by the browser (required, type="email", pattern...) and restored by a form reset. Enter submits the form.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-form-demo',
imports: [...WDS_FIELD, WdsButtonComponent, WdsInputComponent],
template: `
<!-- A plain <form> (no Angular forms module): the browser validates required and type="email". -->
<form class="flex w-sm max-w-full flex-col gap-4" (submit)="submit($event)">
<wds-field-group>
<wds-field>
<wds-field-label>Name</wds-field-label>
<wds-input name="name" placeholder="Jordan Lee" value="Jordan" />
</wds-field>
<wds-field>
<wds-field-label>Email</wds-field-label>
<wds-input name="email" type="email" placeholder="[email protected]" required />
<wds-field-description>We'll send a confirmation to this address.</wds-field-description>
</wds-field>
<div class="flex gap-2">
<wds-button type="reset" variant="outline">Reset</wds-button>
<wds-button type="submit">Submit</wds-button>
</div>
</wds-field-group>
<p class="m-0 font-mono text-sm text-muted-foreground">Submitted: {{ submitted() }}</p>
</form>
`,
})
export class InputFormDemo {
protected readonly submitted = signal('—');
protected submit(e: SubmitEvent): void {
e.preventDefault();
const data = new FormData(e.target as HTMLFormElement);
this.submitted.set(JSON.stringify(Object.fromEntries(data)));
}
}Disabled#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-disabled-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm" disabled>
<wds-field-label>Email</wds-field-label>
<wds-input type="email" placeholder="[email protected]" disabled></wds-input>
<wds-field-description>This field is currently disabled.</wds-field-description>
</wds-field>
`,
})
export class InputDisabledDemo {}Invalid#
invalid on wds-field or on the input shows the error state.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-invalid-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm" invalid>
<wds-field-label>Invalid input</wds-field-label>
<wds-input placeholder="Error"></wds-input>
<wds-field-description>This field contains validation errors.</wds-field-description>
</wds-field>
`,
})
export class InputInvalidDemo {}File#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-file-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Picture</wds-field-label>
<wds-input type="file"></wds-input>
<wds-field-description>Select a picture to upload.</wds-field-description>
</wds-field>
`,
})
export class InputFileDemo {}Inline#
import { Component } from '@angular/core';
import { WdsFieldComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
import { WdsButtonComponent } from '@wds/angular/button';
@Component({
selector: 'app-input-inline-demo',
imports: [WdsFieldComponent, WdsInputComponent, WdsButtonComponent],
template: `
<wds-field class="w-full max-w-sm" orientation="horizontal">
<wds-input type="search" placeholder="Search..." aria-label="Search"></wds-input>
<wds-button>Search</wds-button>
</wds-field>
`,
})
export class InputInlineDemo {}Grid#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-grid-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsInputComponent],
template: `
<div class="grid w-full max-w-sm grid-cols-2 gap-4">
<wds-field>
<wds-field-label>First name</wds-field-label>
<wds-input placeholder="Jordan" autocomplete="given-name"></wds-input>
</wds-field>
<wds-field>
<wds-field-label>Last name</wds-field-label>
<wds-input placeholder="Lee" autocomplete="family-name"></wds-input>
</wds-field>
</div>
`,
})
export class InputGridDemo {}Required#
aria-hidden); required tells screen readers that the field is required.
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldDescriptionComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-required-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsFieldDescriptionComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Required field <span class="text-destructive" aria-hidden="true">*</span></wds-field-label>
<wds-input placeholder="This field is required" required></wds-input>
<wds-field-description>This field must be filled out.</wds-field-description>
</wds-field>
`,
})
export class InputRequiredDemo {}Badge#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsBadgeComponent } from '@wds/angular/badge';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-badge-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsBadgeComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Webhook URL <wds-badge class="ms-auto" variant="secondary">Beta</wds-badge></wds-field-label>
<wds-input type="url" placeholder="https://api.example.com/webhook"></wds-input>
</wds-field>
`,
})
export class InputBadgeDemo {}Input group#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsInputGroupAddonComponent, WdsInputGroupComponent, WdsInputGroupTextComponent } from '@wds/angular/input-group';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-input-group-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsInputGroupComponent, WdsInputGroupAddonComponent, WdsInputGroupTextComponent, WdsInputComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Website URL</wds-field-label>
<wds-input-group>
<wds-input placeholder="example.com"></wds-input>
<wds-input-group-addon><wds-input-group-text>https://</wds-input-group-text></wds-input-group-addon>
<wds-input-group-addon align="inline-end">
<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"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
</wds-input-group-addon>
</wds-input-group>
</wds-field>
`,
})
export class InputInputGroupDemo {}Button group#
import { Component } from '@angular/core';
import { WdsFieldComponent, WdsFieldLabelComponent } from '@wds/angular/field';
import { WdsButtonGroupComponent } from '@wds/angular/button-group';
import { WdsInputComponent } from '@wds/angular/input';
import { WdsButtonComponent } from '@wds/angular/button';
@Component({
selector: 'app-input-button-group-demo',
imports: [WdsFieldComponent, WdsFieldLabelComponent, WdsButtonGroupComponent, WdsInputComponent, WdsButtonComponent],
template: `
<wds-field class="w-full max-w-sm">
<wds-field-label>Search</wds-field-label>
<wds-button-group>
<wds-input placeholder="Type to search..."></wds-input>
<wds-button variant="outline">Search</wds-button>
</wds-button-group>
</wds-field>
`,
})
export class InputButtonGroupDemo {}Forms#
Integration with Angular forms. Pick the API your app uses.
Signal forms#
WdsTextField connects the input to [formField]. Validators from the schema (required, email, minLength...) show their messages through wds-field-error.
import { Component, computed, signal } from '@angular/core';
import { FormField, FormRoot, email, form, required } from '@angular/forms/signals';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_FIELD } from '@wds/angular/field';
import { WdsTextField } from '@wds/angular/forms';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-signal-forms-demo',
imports: [...WDS_FIELD, FormField, FormRoot, WdsButtonComponent, WdsInputComponent, WdsTextField],
template: `
<form class="flex w-sm max-w-full flex-col gap-4" [formRoot]="contactForm">
<wds-field>
<wds-field-label>Name</wds-field-label>
<wds-input placeholder="Jordan Lee" autocomplete="name" [formField]="contactForm.name" />
</wds-field>
<wds-field [invalid]="showEmailError()">
<wds-field-label>Email</wds-field-label>
<wds-input type="email" placeholder="[email protected]" autocomplete="email" [formField]="contactForm.email" />
<wds-field-error [errors]="showEmailError() ? contactForm.email().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 InputSignalFormsDemo {
protected readonly contact = signal({ name: '', email: '' });
protected readonly submitted = signal('—');
protected readonly contactForm = form(
this.contact,
(path) => {
required(path.email, { message: 'Enter your email address.' });
email(path.email, { message: 'Enter a valid email address.' });
},
{
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 showEmailError = computed(() => this.contactForm.email().touched() && this.contactForm.email().invalid());
}Template-driven#
WdsTextValueAccessor makes ngModel work on wds-input. Angular validators (required, email, minlength...) work as on a native input.
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 { WdsTextValueAccessor } from '@wds/angular/forms';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-template-driven-demo',
imports: [...WDS_FIELD, FormsModule, WdsButtonComponent, WdsInputComponent, WdsTextValueAccessor],
template: `
<form #contactForm="ngForm" class="flex w-sm max-w-full flex-col gap-4" (ngSubmit)="save(contactForm)">
<wds-field>
<wds-field-label>Name</wds-field-label>
<wds-input name="name" placeholder="Jordan Lee" autocomplete="name" [(ngModel)]="contact.name" />
</wds-field>
<wds-field [invalid]="!!emailModel.invalid && !!emailModel.touched">
<wds-field-label>Email</wds-field-label>
<wds-input
#emailModel="ngModel"
name="email"
type="email"
placeholder="[email protected]"
autocomplete="email"
[(ngModel)]="contact.email"
required
email
/>
@if (emailModel.invalid && emailModel.touched) {
<wds-field-error>
{{ emailModel.hasError('required') ? 'Enter your email address.' : 'Enter a valid email address.' }}
</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 InputTemplateDrivenDemo {
protected readonly contact = { name: '', 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#
WdsTextValueAccessor 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 { WdsTextValueAccessor } from '@wds/angular/forms';
import { WdsInputComponent } from '@wds/angular/input';
@Component({
selector: 'app-input-reactive-forms-demo',
imports: [...WDS_FIELD, ReactiveFormsModule, WdsButtonComponent, WdsInputComponent, WdsTextValueAccessor],
template: `
<form class="flex w-sm max-w-full flex-col gap-4" [formGroup]="contactForm" (ngSubmit)="save()">
<wds-field>
<wds-field-label>Name</wds-field-label>
<wds-input placeholder="Jordan Lee" autocomplete="name" formControlName="name" />
</wds-field>
<wds-field [invalid]="email.invalid && email.touched">
<wds-field-label>Email</wds-field-label>
<wds-input type="email" placeholder="[email protected]" autocomplete="email" formControlName="email" />
@if (email.invalid && email.touched) {
<wds-field-error>{{ email.hasError('required') ? 'Enter your email address.' : 'Enter a valid email address.' }}</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 InputReactiveFormsDemo {
protected readonly contactForm = new FormGroup({
name: new FormControl('', { nonNullable: true }),
email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
});
protected readonly email = this.contactForm.controls.email;
protected readonly submitted = signal('—');
protected save(): void {
if (this.contactForm.invalid) {
this.contactForm.markAllAsTouched();
return;
}
this.submitted.set(JSON.stringify(this.contactForm.getRawValue()));
}
}API References#
wds-input#
<input> inside; it works in a <form> and with wds-field.
Enter submits the form like a native <input> (the default submit button).
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| type | string | 'text' | text, email, password, search, url, tel, number, file... |
| pattern | string | undefined | ||
| min | string | undefined | number, dates). | |
| max | string | undefined | number, dates). | |
| step | string | undefined | number, dates). | |
| inputmode | string | undefined | numeric, email. | |
| value | string | '' | |
| name | string | '' | |
| placeholder | string | '' | |
| disabled | boolean | false | |
| readonly | boolean | false | |
| required | boolean | false | |
| invalid | boolean | false | aria-invalid. |
| autocomplete | string | '' | autocomplete hint. |
| minlength.minLength | number | undefined | ||
| maxlength.maxLength | number | undefined |
Events
| Event | Description |
|---|---|
| wds-inputCustomEvent<WdsTextChangeDetail> | |
| wds-changeCustomEvent<WdsTextChangeDetail> |
Methods
| Method | Description |
|---|---|
| select() | |
| formResetCallback() | |
| formDisabledCallback(disabled: boolean) | |
| formStateRestoreCallback(state: string | File | FormData | null) | |
| 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(). |
CSS parts (::part)
| Part | Description |
|---|---|
| control | <input>. |
Accessibility#
Screen reader#
wds-input renders a native <input> of the given type, so it is announced as an edit field (or a file button for type="file"). Without wds-field give it a name with aria-label on the element: it is forwarded to the native input.
wds-field, wds-field-label names the input and wds-field-description and wds-field-error describe it. invalid sets aria-invalid="true"; disabled, readonly and required are set on the native input.
Keyboard support#
| Key | Function |
|---|---|
wds-change) and submits the form, as in a native input. |