How to build a login page in Angular
A login page in Angular requires a reactive form with validation, an auth service that calls your backend and stores the JWT, and a route guard that redirects unauthenticated users. As the creator of CoreUI with Angular development experience since 2014, I’ve built the authentication flows in CoreUI Angular templates used by thousands of enterprise developers. The key is separating form logic, HTTP calls, and token storage into distinct layers so each piece is independently testable. A login page that looks professional and handles errors gracefully significantly impacts first impressions of your application.
Build the login form using Angular Reactive Forms and CoreUI components.
// login.component.ts
import { Component } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms'
import { Router } from '@angular/router'
import {
CardModule,
FormModule,
ButtonModule,
GridModule,
AlertComponent
} from '@coreui/angular'
import { AuthService } from '../auth/auth.service'
@Component({
selector: 'app-login',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, CardModule, FormModule, ButtonModule, GridModule, AlertComponent],
template: `
<div class="min-vh-100 d-flex align-items-center">
<c-container>
<c-row class="justify-content-center">
<c-col md="6" lg="5">
<c-card class="p-4">
<c-card-body>
<h1 class="h4 mb-4">Login</h1>
<c-alert color="danger" *ngIf="error">{{ error }}</c-alert>
<form [formGroup]="form" (ngSubmit)="submit()">
<div class="mb-3">
<label cLabel for="email">Email</label>
<input
cFormControl
id="email"
type="email"
formControlName="email"
placeholder="[email protected]"
[class.is-invalid]="isInvalid('email')"
/>
<c-form-feedback *ngIf="isInvalid('email')">
Valid email is required
</c-form-feedback>
</div>
<div class="mb-4">
<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')">
Password must be at least 6 characters
</c-form-feedback>
</div>
<button
cButton
color="primary"
type="submit"
class="w-100"
[disabled]="loading"
>
{{ loading ? 'Signing in...' : 'Sign In' }}
</button>
</form>
</c-card-body>
</c-card>
</c-col>
</c-row>
</c-container>
</div>
`
})
export class LoginComponent {
loading = false
error = ''
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(6)]]
})
constructor(
private fb: FormBuilder,
private auth: AuthService,
private router: Router
) {}
isInvalid(field: string): boolean {
const control = this.form.get(field)
return !!(control?.invalid && control.touched)
}
async submit(): Promise<void> {
if (this.form.invalid) {
this.form.markAllAsTouched()
return
}
this.loading = true
this.error = ''
try {
await this.auth.login(
this.form.value.email!,
this.form.value.password!
)
this.router.navigate(['/dashboard'])
} catch (err: any) {
this.error = err.message ?? 'Login failed. Please try again.'
} finally {
this.loading = false
}
}
}
markAllAsTouched() triggers validation display on all fields when the user submits with empty fields. The isInvalid helper checks if a field has errors AND has been touched — this prevents showing errors before the user has interacted with the field.
Auth Service
Handle JWT storage and HTTP login.
// auth/auth.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { firstValueFrom } from 'rxjs'
@Injectable({ providedIn: 'root' })
export class AuthService {
constructor(private http: HttpClient) {}
async login(email: string, password: string): Promise<void> {
const { token } = await firstValueFrom(
this.http.post<{ token: string }>('/api/auth/login', { email, password })
)
localStorage.setItem('token', token)
}
logout(): void {
localStorage.removeItem('token')
}
getToken(): string | null {
return localStorage.getItem('token')
}
isLoggedIn(): boolean {
return !!this.getToken()
}
}
Route Guard
Redirect unauthenticated users to the login page.
// guards/auth.guard.ts
import { inject } from '@angular/core'
import { Router } from '@angular/router'
import { AuthService } from '../auth/auth.service'
export const authGuard = () => {
const auth = inject(AuthService)
const router = inject(Router)
return auth.isLoggedIn() ? true : router.parseUrl('/login')
}
Best Practice Note
This is the exact login page structure used in CoreUI Angular Admin Template, which includes a polished login page with a full authentication flow ready to connect to your backend. For production, add token refresh logic, handle token expiry, and consider using sessionStorage instead of localStorage for stricter security. See how to build a signup page in Angular for the companion registration flow.



