Angular 20
Paving the way to exciting features
Signal-driven, reactive, and zoneless
Nicky Haze - 6 min read
The Angular team keeps moving fast and with v21 (released November 2025) we're entering the next stage of Angular becoming even more reactive and zoneless. Angular 20 stabilised their signals and zoneless APIs and Angular 21 now comes with some features build on top of that foundation, introducing: Signal Forms, expanding zoneless support, and improving SSR and rendering performance.
This is a release with a clear view of where Angular is going, and it actually continues to move more towards a more "signal-driven" framework.
In my experience Forms have always been quite complex in the Angular framework. Back in the days Reactive Forms were great and gave us structure, but this structure came with quite some boilerplate and dependencies on RxJS. With Signal Forms (Developer Preview) Angular 21 introduces managing form state using signals.
This new approach means:
FormGroup/FormControl hierarchiestouched, dirty, invalid) as signalsField directiveimport { Component, signal } from '@angular/core'; import { form, schema, required, Field } from '@angular/forms/signals'; @Component({ selector: 'app-profile-form', imports: [Field], template: ` <form (submit)="onSubmit($event)"> <label>First name</label> <input [field]="profileForm.name.first" /> @if (profileForm.name.first().invalid()) { <span>Required</span> } <label>Last name</label> <input [field]="profileForm.name.last" /> <label>Username</label> <input [field]="profileForm.username" /> <label>Email</label> <input type="email" [field]="profileForm.email" /> <label>Age</label> <input type="number" [field]="profileForm.age" /> <button type="submit">Save</button> </form> `, }) export class ProfileFormComponent { readonly profile = signal({ name: { first: '', last: '' }, username: '', email: '', age: null as number | null, }); readonly profileForm = form(this.profile, (p) => { // Name schema schema(p.name, (n) => { required(n.first, { message: 'First name is required' }); required(n.last, { message: 'Last name is required' }); }); required(p.username, { message: 'Username is required' }); required(p.email, { message: 'Email is required' }); required(p.age, { message: 'Age is required' }); }); onSubmit(event: Event): void { console.log('Submitted', this.profile()); } }
As you can see the magic here lies in form() and schema(). Instead of manually connecting FormControls, you simply describe what the form looks like, and Angular does the rest. Love it!
Validation also becomes declarative. Here's how to create a custom validator that checks for the user being at least 18 years old:
// age.validator.ts import { Field, ValidationError, validate } from '@angular/forms/signals'; export function adultValidator(field: Field<number | null>) { validate(field, (ctx) => { const age = ctx.value(); if (age === null || Number.isNaN(age)) { return null; } return age >= 18 ? [] : [ { kind: 'min_age', message: 'You must be at least 18 years old.', } satisfies ValidationError, ]; }); }
Using it inside the form becomes very easy and clean:
readonly profileForm = form(this.profile, (p) => { // Name schema schema(p.name, (n) => { required(n.first, { message: 'First name is required' }); required(n.last, { message: 'Last name is required' }); }); required(p.username, { message: 'Username is required' }); required(p.email, { message: 'Email is required' }); required(p.age, { message: 'Age is required' }); // Apply custom validator adultValidator(p.age); });
The template can read errors() directly from the field signal:
@if (profileForm.age().invalid()) { <span>{{ profileForm.age().errors()[0]?.message }}</span> }
For API-driven validation (e.g. to check if a username is unique) Angular 21 provides validateHttp(). It lets you define a request and convert the result into validation errors without dealing with RxJS subscriptions.
// username-unique.validator.ts import { Field, ValidationError, validateHttp, customError, } from '@angular/forms/signals'; interface UsernameCheckResult { unique: boolean; } export function userNameUniqueValidator(field: Field<string>) { validateHttp<UsernameCheckResult>(field, { request: ({ value }) => { const username = value(); if (!username) return null; return { url: `https://api.example.com/check/${encodeURIComponent(username)}`, method: 'GET', }; }, errors: (result): ValidationError[] => { if (!result) return []; return result.unique ? [] : [ customError({ kind: 'notUnique', message: 'Username already taken', }), ]; }, }); }
Using it inside the form again becomes very easy and clean:
readonly profileForm = form(this.profile, (p) => { // Name schema schema(p.name, (n) => { required(n.first, { message: 'First name is required' }); required(n.last, { message: 'Last name is required' }); }); required(p.email, { message: 'Email is required' }); required(p.username, { message: 'Username is required' }); // Apply custom async validator usernameUniqueValidator(p.username); required(p.age, { message: 'Age is required' }); // Apply custom validator adultValidator(p.age); });
This new API makes it easy to create reusable validators that work directly with signals.
Even though Signal Forms are still experimental in v21, they're already awesome to work with. I expect it to become the default way to handle forms in Angular in the near future.
Starting with Angular 21, zoneless change detection is now enabled by default. No more Zone.js dependency. The Zoneless API has been stable since Angular 20.2, but version 21 takes it further: there's no need to import provideZonelessChangeDetection() in your app config, as all new Angular applications are now zoneless out of the box.
Without zone.js, Angular no longer needs to patch async APIs to track changes. Combining this with signals, the benefits are:
Most first-party libraries now support zoneless mode and third-party library support is growing too.
Angular 21 improves Server-Side Rendering (SSR) and hydration with:
Example configuration client and server routes with different rendering strategies:
// app.routes.server.ts import { RenderMode, ServerRoute } from '@angular/ssr'; export const serverRoutes: ServerRoute[] = [ {path: '', renderMode: RenderMode.Client}, // Rendered on the client like a typical SPA {path: 'login', renderMode: RenderMode.Server}, // Always rendered on the server {path: 'dashboard', renderMode: RenderMode.Client}, // Rendered on the client like a typical SPA {path: '**', renderMode: RenderMode.Prerender}, // This page is static, so it's pre-rendered during build time ];
// app.config.server.ts import { ApplicationConfig } from '@angular/core'; import { provideServerRendering, withRoutes } from '@angular/ssr'; import { serverRoutes } from './app.routes.server'; export const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(withRoutes(serverRoutes)), ], };
Example client configuration:
// app.routes.ts (client) import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./home/home').then(m => m.HomeComponent), title: 'Home' }, { path: 'login', loadComponent: () => import('./auth/login').then(m => m.LoginComponent), title: 'Login' }, { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard').then(m => m.DashboardComponent), title: 'Dashboard' }, { path: '**', loadComponent: () => import('./shared/not-found').then(m => m.NotFoundComponent), title: 'Not Found' } ];
// app.config.ts (client) import { ApplicationConfig } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [provideRouter(routes)], };
Angular now allows you to choose the optimal rendering strategy per route, which is great for performance and SEO.
Until Angular 20, you always had to add:
import { HttpClientModule } from '@angular/common/http'; bootstrapApplication(AppComponent, { providers: [provideHttpClient()] });
But in Angular 21, HttpClient is included by default. No more extra imports.
Angular 21 introduces Angular ARIA (developer preview). Angular ARIA is a library of unstyled directives that implement accessibility patterns especially for when you don't want to rely on Angular CDK (since it's large).
// before <div role="menu" aria-label="Main menu"> <div role="menuitem" tabindex="0" aria-selected="false">Home</div> <div role="menuitem" tabindex="-1" aria-selected="false">About us</div> <div role="menuitem" tabindex="-1" aria-selected="false">Contact us</div> </div> // After: <div ngMenu> <ng-template ngMenuContent> <div ngMenuItem>Home</div> <div ngMenuItem>About us</div> <div ngMenuItem>Contact us</div> </ng-template> </div>
<div ngListbox> @for (item of items(); track item.id) { <div [value]="item.value" ngOption>{{ item.name }}</div> } </div>
These are the available directives:
Checkout these examples for more information.
angular.json continues to shrink, removing legacy build options.This isn't everything Angular 21 brings, check out the official Angular 21 release notes and Angular GitHub changelog for more.
Even though this release isn't revolutionary I'm excited to see what Angular 21 offers. The introduction of Signal Forms, focus on signals and zoneless architecture makes Angular a modern, efficient framework for building web applications. If you're using Angular, upgrading to v21 is definitely worth considering.