Complete guide: Angular Reactive Forms Validation — Complete Guide (2026)
GitHub · LinkedIn · About · YouTube
Last updated by Kindson Munonye — June 29, 2026
📚 Tutorial hub: Angular tutorials · CRUD series
Related: Template-driven forms · Reactive forms
Prerequisites: Reactive forms tutorial recommended.
Estimated time: ~40 minutes · Last updated: June 29, 2026
Try it live: Open StackBlitz demo (editable Angular sandbox)
📚 Browse all tutorials: Angular tutorials hub
This simple article explains form validation in Angular. You’ll also learn how to display messages to the user based on validation errors.
This tutorial would be step by step.
- About HTML Form Validation
- Setup the Form
- Submit Function
- Validation Styles
- Validation Messages
- More on Validation
1. Native HTML Form Validation
Without Angular, we have a native HTML form validation. You can decide to use in your angular application. One benefit is that it is very easy to use.
To use the native HTML form validation, just include ngNativeValidation to the form tag. Then any validation markup would be handled. For example including required to the input would cause a required field validation.
2. Set up Your Form
I have setup a basic html form. This is shown below.
<form #f="ngForm" (ngSubmit)="onSubmit(f)"> <div class="form-group"> <label>Firstname</label> <div class="input-group"> <input class="form-control" name="firstname" required> </div> </div> <div class="form-group"> <label>Lastname</label> <div class="input-group"> <input name="lastname" class="form-control" ngModel /> </div> </div> <div class="form-group"> <label >Department</label> <div class="input-group"> <input name="department" class="form-control" ngModel> </div> </div> <div class="form-group"> <label>Email</label> <div class="input-group"> <input name="email" class="form-control" ngModel> </div> </div> <div class="form-group"> <label for="country">Country</label> <div class="input-group"> <select id="country" name="country" class="form-control" ngModel> <option *ngFor="let country of countries" value={{country}}> {{country}} </option> </select> </div> </div> <div class="border text-center"> <button type="submit" data-dismiss="modal" class="btn btn-info">Submit</button> </div> </form> </div>
Note the tree attributes for each control. For firstname, for instance:
- name = “firstname” – requirement for using ngModel
- NgModel – creates a binding with the model
- #firstname = ngModel : helps us get the value of the particular control
Like this
name="firstname" #firstname="ngModel" ngModel
Do this for all the other controls.
Note: It’s easy to confuse NgModel with ngModel. So beware!
3. Write the Submit function
Unlike native html validation, the Angular validation would execute the submit function regardless of whether the form entries are valid or not. However, you can easily check the form for validity before executing the actual submit code.
The onSubmit() function below does this.
onSubmit(f: NgForm) { if (f.valid) { this.message = 'The form is VALID'; } if (f.invalid){ this.message = 'The form is INVALID'; } }
4. Valid and Invalid Styles
Now we would like to add a style for the inputs based on if it is valid. This style is shown below. But you can modify it as you wish.
input.ng-invalid{ border-left: 5px solid red; } input.ng-valid{ border-left: 5px solid green; }
So now, if you test out the application, you’ll see that if the field are empty, invalid style is applied. And vice versa.
5. Validation Messages
Now we would like to display validation message if an invalid input is detected.
You can add a message for each control. Or you can add a general message for all the controls.
So go ahead to add a <span></span> tag after the firstname. Give this a class, say, error.
<span *ngIf="firstname.invalid" class="error">You must enter firstname</span>
Then define the error class.
.error{ color: red; }
Touched and Untouched
To prevent display of the error message when page reload, add this condition to the *ngIf:
*ngIf="firstname.invalid && firstname.touched"
6. Min, Max, and Pattern Validators
For numeric fields, use Validators.min() and Validators.max(). For pattern matching (letters only, phone format), use Validators.pattern():
phone: ['', [Validators.required, Validators.pattern(/^\d{10}$/)]],
username: ['', [Validators.required, Validators.pattern(/^[a-zA-Z]+$/)]]7. Email and Password Validation
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
confirmPassword: ['', Validators.required]For password confirmation, use a cross-field validator on the FormGroup:
function passwordMatch(group: AbstractControl): ValidationErrors | null {{
const pass = group.get('password')?.value;
const confirm = group.get('confirmPassword')?.value;
return pass === confirm ? null : {{ passwordMismatch: true }};
}}
this.form = this.fb.group({{ password: [''], confirmPassword: [''] }}, {{ validators: passwordMatch }});8. Custom Validators (Complete Example)
Custom validators are functions that return null (valid) or an error object:
export function forbiddenName(name: string): ValidatorFn {{
return (control: AbstractControl): ValidationErrors | null => {{
return control.value === name ? {{ forbiddenName: {{ value: control.value }} }} : null;
}};
}}
// Apply to control
firstname: ['', [Validators.required, forbiddenName('admin')]]Display custom errors in the template:
<span *ngIf="firstname.hasError('forbiddenName')">That name is not allowed</span>9. Validation in Reactive Forms
The same validators work identically in reactive forms. Access the form with formControlName and check .errors on each control. Use markAllAsTouched() on submit to reveal all messages at once.
Practice: Fork the StackBlitz validation demo and add your own custom validator.

[…] can be a bit more creative with validation by displaying nicer styles like shown below. See my Validation Tutorial and step by step […]
[…] Continue the series: Part 2: Reactive Forms · Form validation guide […]
[…] Continue the series: Part 1: Template-driven forms · Form validation guide […]
Every day is a new beginning
So true!