Angular Material’s form components (mat-form-field, mat-input, mat-select, mat-datepicker) integrate seamlessly with both template-driven and reactive forms. They provide consistent, accessible, and visually polished form UI with built-in error state display through <mat-error>. For the BlogApp’s login and registration forms, Angular Material provides the visual polish of a production-quality auth UI without writing custom CSS for every form element.
Complete Material Login Form
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
@Component({
selector: 'app-login',
standalone: true,
imports: [
FormsModule,
MatFormFieldModule,
MatInputModule,
MatButtonModule,
MatIconModule,
MatCheckboxModule,
],
template: `
<div class="login-card">
<h1>Sign In</h1>
<form #loginForm='ngForm' (ngSubmit)="onSubmit(loginForm)" novalidate>
<!-- mat-form-field wraps the input and provides consistent styling โโ -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Email Address</mat-label>
<mat-icon matPrefix>email</mat-icon>
<input matInput
type="email"
name="email"
[(ngModel)]="credentials.email"
required
email
#emailCtrl="ngModel"
autocomplete="email">
<!-- mat-error shown automatically when control is invalid+touched โ -->
<mat-error *ngIf="emailCtrl.hasError('required')">Email is required</mat-error>
<mat-error *ngIf="emailCtrl.hasError('email')">Enter a valid email</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Password</mat-label>
<mat-icon matPrefix>lock</mat-icon>
<input matInput
[type]="hidePassword ? 'password' : 'text'"
name="password"
[(ngModel)]="credentials.password"
required
minlength="8"
#passwordCtrl="ngModel"
autocomplete="current-password">
<!-- Toggle password visibility โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<button mat-icon-button matSuffix type="button"
(click)="hidePassword = !hidePassword"
[attr.aria-label]="hidePassword ? 'Show password' : 'Hide password'">
<mat-icon>{{ hidePassword ? 'visibility_off' : 'visibility' }}</mat-icon>
</button>
<mat-error *ngIf="passwordCtrl.hasError('required')">Password is required</mat-error>
<mat-error *ngIf="passwordCtrl.hasError('minlength')">
Minimum 8 characters required
</mat-error>
</mat-form-field>
<!-- mat-checkbox โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
<mat-checkbox name="rememberMe" [(ngModel)]="rememberMe">
Remember me
</mat-checkbox>
<!-- Server error โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ -->
@if (serverError()) {
<mat-error class="server-error">{{ serverError() }}</mat-error>
}
<button mat-raised-button
color="primary"
type="submit"
class="full-width"
[disabled]="loginForm.invalid || isSaving()">
@if (isSaving()) {
<mat-spinner diameter="20" />
} @else {
Sign In
}
</button>
</form>
<p class="register-link">
Don't have an account?
<a routerLink="/auth/register">Create one</a>
</p>
</div>
`,
styles: [`
.full-width { width: 100%; }
.login-card { max-width: 400px; margin: 4rem auto; padding: 2rem; }
.server-error { display: block; margin: 0.5rem 0; }
`],
})
export class LoginMaterialComponent {
credentials = { email: '', password: '' };
rememberMe = false;
hidePassword = true;
isSaving = signal(false);
serverError = signal('');
// ... onSubmit logic
}
<mat-error> inside a <mat-form-field> is shown automatically when the bound form control is in an invalid and touched state โ Material handles the visibility logic. You do not need @if (control.invalid && control.touched) around mat-error elements; Material’s error state machine manages that. The hasError('validatorName') method inside mat-error determines which specific message shows โ you can have multiple mat-error elements, one per validator failure.appearance="outline" on mat-form-field for the most legible and modern Material Design 3 form style. The fill appearance is also popular. Set this globally in the Material theme to avoid repeating it on every form field. Add a global style for .full-width { width: 100%; } and apply this class to all form fields so they expand to fill the form container โ the default Material form field width is set by the content.mat-error elements use the *ngIf structural directive syntax (not @if) in most examples because Angular Material templates are written in a style compatible with both NgModule and standalone APIs. Using *ngIf within a standalone component’s template works fine as long as NgIf or CommonModule is imported. The new @if syntax works equally well โ choose consistency with the rest of your codebase.Common Mistakes
Mistake 1 โ Forgetting matInput directive on inputs inside mat-form-field
โ Wrong โ <input type="text"> without matInput inside mat-form-field; no Material styling applied.
โ
Correct โ always add the matInput directive: <input matInput ...>.
Mistake 2 โ Manually showing/hiding mat-error with @if (double-shown errors)
โ Wrong โ wrapping mat-error in @if (control.invalid && control.touched); Material already handles visibility.
โ Correct โ put mat-error directly inside mat-form-field; Angular Material manages when it appears.