How to build a signup page in Angular
A signup page in Angular requires a reactive form with custom validators for password confirmation, client-side validation before submission, and an HTTP call to register the user on the backend. As the creator of CoreUI with Angular development experience since 2014, I designed the registration components in CoreUI Angular templates that handle the complete sign-up flow including error handling and success redirects. The most important custom validation pattern is the password confirmation check — Angular’s built-in validators don’t cover cross-field validation, so you must write a custom group validator. This validator compares two fields and marks the confirmation field as invalid if they don’t match.
Create a signup form with custom password match validation.
// signup.component.ts
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ReactiveFormsModule, FormBuilder, Validators, AbstractControl, ValidationErrors } from '@angular/forms'
import { Router, RouterLink } from '@angular/router'
import { CardModule, FormModule, ButtonModule, GridModule } from '@coreui/angular'
import { AuthService } from '../auth/auth.service'
// Custom validator: checks password === confirmPassword
function passwordMatchValidator(group: AbstractControl): ValidationErrors | null {
const password = group.get('password')?.value
const confirm = group.get('confirmPassword')?.value
return password === confirm ? null : { passwordMismatch: true }
}
@Component({
selector: 'app-signup',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, CardModule, FormModule, ButtonModule, GridModule, RouterLink],
template: `
<div class="min-vh-100 d-flex align-items-center">
<c-container>
<c-row class="justify-content-center">
<c-col md="7" lg="6">
<c-card class="p-4">
<c-card-body>
<h1 class="h4 mb-4">Create Account</h1>
<form [formGroup]="form" (ngSubmit)="submit()">
<c-row>
<c-col sm="6">
<div class="mb-3">
<label cLabel for="firstName">First Name</label>
<input cFormControl id="firstName" formControlName="firstName"
[class.is-invalid]="isInvalid('firstName')" />
<c-form-feedback *ngIf="isInvalid('firstName')">Required</c-form-feedback>
</div>
</c-col>
<c-col sm="6">
<div class="mb-3">
<label cLabel for="lastName">Last Name</label>
<input cFormControl id="lastName" formControlName="lastName"
[class.is-invalid]="isInvalid('lastName')" />
<c-form-feedback *ngIf="isInvalid('lastName')">Required</c-form-feedback>
</div>
</c-col>
</c-row>
<div class="mb-3">
<label cLabel for="email">Email</label>
<input cFormControl id="email" type="email" formControlName="email"
[class.is-invalid]="isInvalid('email')" />
<c-form-feedback *ngIf="isInvalid('email')">Valid email required</c-form-feedback>
</div>
<div class="mb-3">
<label cLabel for="password">Password</label>
<input cFormControl id="password" type="password" formControlName="password"
[class.is-invalid]="isInvalid('password')" />
<c-form-feedback *ngIf="isInvalid('password')">
At least 8 characters
</c-form-feedback>
</div>
<div class="mb-4">
<label cLabel for="confirmPassword">Confirm Password</label>
<input cFormControl id="confirmPassword" type="password" formControlName="confirmPassword"
[class.is-invalid]="showPasswordMismatch" />
<c-form-feedback *ngIf="showPasswordMismatch">
Passwords do not match
</c-form-feedback>
</div>
<p *ngIf="error" class="text-danger">{{ error }}</p>
<button cButton color="success" type="submit" class="w-100" [disabled]="loading">
{{ loading ? 'Creating account...' : 'Create Account' }}
</button>
</form>
<div class="mt-3 text-center">
Already have an account? <a routerLink="/login">Sign in</a>
</div>
</c-card-body>
</c-card>
</c-col>
</c-row>
</c-container>
</div>
`
})
export class SignupComponent {
loading = false
error = ''
form = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]
}, { validators: passwordMatchValidator })
constructor(
private fb: FormBuilder,
private auth: AuthService,
private router: Router
) {}
get showPasswordMismatch(): boolean {
const confirm = this.form.get('confirmPassword')
return !!(confirm?.touched && this.form.errors?.['passwordMismatch'])
}
isInvalid(field: string): boolean {
const c = this.form.get(field)
return !!(c?.invalid && c.touched)
}
async submit(): Promise<void> {
if (this.form.invalid) {
this.form.markAllAsTouched()
return
}
this.loading = true
this.error = ''
try {
await this.auth.register(this.form.value)
this.router.navigate(['/dashboard'])
} catch (err: any) {
this.error = err.message ?? 'Registration failed. Please try again.'
} finally {
this.loading = false
}
}
}
The passwordMatchValidator is passed as a group validator, not a field validator. This gives it access to both password and confirmPassword. The showPasswordMismatch getter checks the group-level error only after the confirmation field has been touched.
Best Practice Note
This is the registration form pattern used in CoreUI Angular Admin Template — the complete auth flow is included out of the box. For production, add email verification: create the user with verified: false, send a verification email with a token, and set verified: true when the user clicks the link. See how to build a login page in Angular for the companion login flow.



