Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

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.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author