Angular v22 Is Here: Signal Forms Go Stable, Async Signals Land, Aria Ships [2026]
Angular v22 shipped officially — the biggest release for the signal era of the framework. After two cycles of experimental APIs, three of the headline features the Angular team has been previewing all year — Signal Forms, Asynchronous Signals, and Angular Aria — are now stable and production-ready. Add to that a swath of template ergonomics, security hardening, AI tooling, and a series of small DX wins, and v22 is the most polished single Angular release in years.
Related: Want to try the AI tooling mentioned here? Set up the Angular MCP server walks through Claude, Cursor and VS Code.
This post is the deep summary. We will walk every meaningful change in v22, what it replaces, how to migrate, and which features matter most for your codebase. If you maintain an Angular app, this is your two-page brief on what the release means for you.
TL;DR — three headline stabilizations #
The headline of v22 is one word: stable. Three APIs that have been experimental graduate to production-ready:
| API | What it does | Replaces |
|---|---|---|
| Signal Forms | A reactive form API built on signals with automatic two-way binding, type-safe field access, and schema-based validation | Reactive Forms (the FormGroup/FormControl API) and template-driven (ngModel) for most new code |
| Asynchronous Signals | The Resource APIs — resource(), httpResource(), rxResource() — graduate to stable, joined by the new injectAsync() and the experimental debounced() |
Hand-rolled effect() + Promise patterns; some RxJS pipelines |
| Angular Aria | Headless, accessible directives for common UI patterns — accordion, combobox, grid, listbox, menu, tabs, toolbar, tree — that you style yourself | Hand-built ARIA widgets assembled from @angular/cdk/a11y building blocks |
All three were previewed in v21 — Signal Forms and the Resource APIs as experimental, Angular Aria as developer preview. v22 removes those labels, so they are now covered by Angular's normal semver and deprecation policy.
Signal Forms — finally stable #
The v21 announcement called Signal Forms "experimental but production-ready in practice." v22 makes it official. The API is now exported from @angular/forms/signals and stable.
The shape (covered in detail in lesson 4.1 of the Angular tutorial):
import { form, FormField, required, email, minLength } from '@angular/forms/signals';
this.loginModel = signal({ email: '', password: '' });
this.loginForm = form(this.loginModel, (s) => {
required(s.email);
email(s.email);
minLength(s.password, 8);
});
// Template (add FormField to the component's imports)
// <input type="email" [formField]="loginForm.email" />
// Read state
this.loginForm.email().value(); // current value (signal)
this.loginForm.email().errors(); // [{ kind: 'required', ... }]
this.loginForm().valid(); // boolean signal
What changed between v21 experimental and v22 stable:
- Stable guarantees. In v21 the API was marked experimental and could change between releases. In v22 it is covered by Angular's normal semver and deprecation policy. If you tried an early preview, the binding is now
[formField]from theFormFielddirective, andFormRoothandles<form>submission. - Schema-based validators are first-class. Validation rules —
required(),validate(),validateHttp()and friends — bind to field paths in a schema function passed toform(), so sync, async and cross-field rules share one shape. - Type-safe field access.
form.email().value()is strongly typed; you cannot read a non-existent field. - Automatic two-way binding through
[formField]. Native inputs sync with the model out of the box, and custom controls join in by implementingFormValueControlwith avalue = model()signal.
For brand-new forms in 2026, this is the recommended API. Reactive Forms continue to work and are not deprecated, but the Angular team's recommended migration path for new code is signal forms.
If you're starting a new form today: reach for form() from @angular/forms/signals. If you're maintaining a reactive-forms codebase: migrate opportunistically when you touch each form.
Asynchronous Signals — stable primitives for async #
The second big stabilization is the Resource API family. resource(), httpResource() and rxResource() are now stable (their typings are marked @publicApi 22.0), and v22 adds new pieces around them:
debounced() #
New in v22: give it a signal and a wait time, and it returns a Resource whose value() settles to the source value after N milliseconds of stillness:
import { debounced, signal } from '@angular/core';
query = signal('');
debouncedQuery = debounced(this.query, 300); // call in an injection context
// query.set('a'); query.set('an'); query.set('ang');
// debouncedQuery.status() is 'loading' while the timer runs;
// 300ms after the last set, debouncedQuery.value() is 'ang'
Unlike the rest of this section, debounced() is experimental in v22, so its API may still change. The most common use — feeding debouncedQuery.value() into httpResource() for search-as-you-type — is covered in lesson 3.4 of the Angular tutorial.
linkedSignal() (stable since v20) #
Writable derived state with auto-reset when its source changes — for active-tab indices, dependent form defaults, and other "derive-but-also-let-the-user-override" patterns. Not new in v22, but it pairs naturally with resources. Covered in lesson 3.8.
injectAsync() #
A new, stable helper for lazy-loading a service. You pass a function that dynamically imports the service class; injectAsync() returns a function that resolves the instance on demand:
import { injectAsync, onIdle } from '@angular/core';
export class ReportPage {
private exporter = injectAsync(
() => import('./pdf-exporter.service').then((m) => m.PdfExporter),
{ prefetch: onIdle },
);
async exportPdf() {
(await this.exporter()).export();
}
}
The service code stays out of the initial bundle until it is first needed (or until the browser is idle, with the onIdle prefetch trigger). The service must be auto-provided — @Injectable({ providedIn: 'root' }) or @Service() — and injectAsync() must be called in an injection context.
Sync values in Resource #
Streaming resources (resource({ stream }) and rxResource(), lesson 6.3) can now deliver their value synchronously instead of always starting in 'loading'. That is what makes SSR caching work (see "Resource caching for SSR" below): a resource restored from TransferState starts out 'resolved', so hydration does not throw away the server-rendered DOM (lesson 9.6).
Angular Aria — accessibility primitives shipped #
The third stabilization: Angular Aria — the @angular/aria package, which provides headless, accessible directives for common interactive patterns. You bring the markup and styles; Aria handles keyboard interaction, focus and ARIA attributes. The v22 package ships these entry points:
- Accordion —
@angular/aria/accordion - Combobox —
@angular/aria/combobox(the formerSimpleCombobox, now promoted toCombobox) - Grid, Listbox and Tree —
@angular/aria/grid,@angular/aria/listbox,@angular/aria/tree - Menu, Tabs and Toolbar —
@angular/aria/menu,@angular/aria/tabs,@angular/aria/toolbar
In v22 the developer preview label was removed and every pattern gained component test harnesses. The promotion brings two breaking changes: the legacy combobox and autocomplete implementations were removed in favor of the new standalone combobox, and the values input/model was renamed to value (for Signal Forms compatibility).
@angular/cdk/a11y is not deprecated — FocusTrap, LiveAnnouncer and the key managers remain the lower-level toolkit. It did get its own v22 breaking changes, such as the injector parameter of the FocusTrap constructors becoming required.
For anyone shipping custom UI widgets (a custom dropdown, autocomplete, tree view), this is the toolkit.
Template enhancements #
v22 ships several template-layer improvements:
Safe navigation narrows nullables #
Template type-checking now understands that optional chaining short-circuits the whole chain. In {{ user?.address.city }}, where user is nullable but address is not, earlier versions with strictTemplates reported "Object is possibly 'undefined'" on city. v22 narrows correctly:
<!-- user: User | null, and User.address is not optional -->
<p>{{ user?.address.city }}</p> <!-- previously flagged; now type-checks -->
Because the checker is now more precise, existing projects may see new nullishCoalescingNotNullable and optionalChainNotNullable diagnostics for ?. and ?? that are no longer needed. ng update temporarily disables those two diagnostics so you can clean them up at your own pace.
Optional chaining returns undefined (not null) #
A subtle but useful change: in Angular template expressions, obj?.x now matches TypeScript semantics — returns undefined if the left side is nullish, not null. Matches what your TypeScript code does outside templates. Reduces "why is it undefined here and null there?" debugging.
Comments in HTML elements #
You can now write JavaScript-style comments — // and /* */ — inside an element's attribute list:
<button
type="submit"
// TODO: add aria-label once design lands
/* submits the parent form */
(click)="submit()"
>
Minor quality-of-life win for annotating long attribute lists.
Developer experience #
A grab-bag of API improvements that quietly make daily work better:
@Service decorator #
A new, stable decorator for service classes — a shorthand for @Injectable({ providedIn: 'root' }):
import { Service } from '@angular/core';
@Service()
export class AuthService {
user = signal<User | null>(null);
// ...
}
Two differences matter: @Service() supports only inject()-based dependencies (no constructor injection), and it replaces useClass / useValue / useExisting / useFactory with a single factory option. Use @Service({ autoProvided: false }) when you want to provide it yourself. ng generate @angular/core:service-migration converts eligible @Injectable classes.
TestBed.getLastFixture #
The new helper returns the most recently created ComponentFixture — handy when the fixture is created in a beforeEach and a test needs it:
beforeEach(() => {
TestBed.createComponent(MyComponent);
});
it('renders the title', async () => {
const fixture = TestBed.getLastFixture<MyComponent>();
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('Hello');
});
It throws if no fixture has been created yet. An early version of this API was called TestBed.getFixture; the shipped name is getLastFixture.
ApplicationRef.bootstrap with config #
The imperative bootstrap API (used for dynamically-rendered components, lesson 2.14) now accepts an options object with the same shape as createComponent — hostElement, directives and bindings:
import { inputBinding } from '@angular/core';
this.appRef.bootstrap(ConfirmDialog, {
hostElement: '#dialog-root',
bindings: [inputBinding('title', () => 'Confirm')],
});
Passing a host element as the plain second argument still works, but it no longer accepts any, so a nullable element is now a type error.
Host directives de-duplication #
If the same directive ends up matching one element more than once — for example, once in the template and again as a host directive — v22 de-duplicates the matches (a template match wins over host-directive matches). Before v22 this threw an error, which limited how freely libraries could reuse host directives. No code change needed.
Enhanced profiling with doc URLs #
Angular's integration with the Chrome DevTools Performance panel now links lifecycle hooks and other Angular profiler events to the relevant documentation on angular.dev. Useful when diagnosing CD performance issues for the first time.
Performance and security #
Three security/perf improvements worth flagging:
Resource caching for SSR #
resource() (lesson 6.3) gains an id option. When it is set, Angular stores the resolved value in TransferState on the server and uses it to initialize the resource in the 'resolved' state on the client, so the loader does not run again during hydration (lesson 9.6):
userResource = resource({
params: () => ({ id: this.userId() }),
loader: ({ params }) => fetchUser(params),
id: 'user-profile',
});
The id must be unique and identical on server and client. Because the value is serialized into the HTML, avoid it for user-specific data when the rendered page can be cached or shared.
Sanitization of SVG href / xlink:href #
Dynamic href and xlink:href bindings on SVG <a> elements are now sanitized for javascript: URLs the same way other URL contexts are. Cuts another XSS vector.
Sanitization of custom-namespaced elements #
Elements with a custom namespace prefix (for example <xhtml:a>) are now checked against the same property schema and sanitization rules as their plain HTML counterparts. Previously a dynamic href on such an element skipped URL sanitization.
The two sanitization fixes need no code changes — they harden the framework's defaults. SSR caching is opt-in per resource.
AI integration — provideExperimentalWebMcpTools #
The Angular team continues to lean into AI-augmented development. v22 introduces:
- An in-page DI graph tool for AI agents —
@angular/corenow registers AI runtime debugging tools in the page, includingangular:di-graph, which returns the application's dependency injection graph (element and environment injectors). Useful for answering "why is THIS instance being injected?" questions. provideExperimentalWebMcpTools— an experimental provider (with the lower-leveldeclareExperimentalWebMcpTool) for declaring WebMCP tools. Each tool has a name, description, JSON input schema and anexecutefunction that runs in the injection context of the injector it was provided to, so it caninject()your services. AI agents in the browser that support WebMCP can then call into your app. This is separate fromng mcp(covered in lesson 12.1), which serves your workspace to coding assistants.- The
ng mcpserver — the Angular CLI MCP server's stable tools includelist_projects,get_best_practices,search_documentation,ai_tutor,onpush_zoneless_migration,run_targetand the dev-server tools (devserver_start,devserver_stop,devserver_wait_for_build).
If you're using Claude, Cursor, or another MCP-aware AI assistant alongside an Angular project, v22's tooling significantly upgrades what the AI can do for you.
Breaking changes #
The v22 release notes call out two breaking changes worth noting:
TypeScript 5.9 support dropped #
Angular v22 requires TypeScript 6.0 — @angular/compiler-cli 22 declares a peer dependency of >=6.0 <6.1. If you're on 5.9 or older, upgrade TypeScript as part of the move: run ng update @angular/cli @angular/core and make sure typescript ends up on a 6.0.x version.
Components default to OnPush #
A component without an explicit changeDetection setting now uses OnPush. To keep the previous behavior, set changeDetection: ChangeDetectionStrategy.Eager. ng update runs a migration that adds ChangeDetectionStrategy.Eager to existing components, so behavior does not change silently.
Both changes are mechanical fixes. No deep refactoring required. The full changelog lists more — for example, the router's paramsInheritanceStrategy now defaults to 'always', ComponentFactoryResolver is gone, and the Hammer.js integration was removed — so read it before upgrading a large app.
Deprecations #
v22 doesn't deprecate any major user-facing APIs. The notable deprecations are in @angular/common/http:
withFetch()— the Fetch-based backend is now the defaultHttpBackend, sowithFetch()is deprecated and can simply be removed. If you need upload progress events, opt back into XHR withprovideHttpClient(withXhr()); anng updatemigration adds it whereHttpXhrBackendis in use.reportProgress— deprecated in favor of the separatereportUploadProgressandreportDownloadProgressoptions.
Nothing in v22 forces a rewrite. The deprecation runway is long; legacy code can stay legacy for several major releases.
Node.js 26 support #
The Angular CLI now supports Node.js 26. The v22 packages require Node.js ^22.22.3, ^24.15.0 or >=26.0.0 — Node.js 20 is no longer supported, so upgrade Node before you upgrade Angular.
Migration: what to do today #
Three concrete steps for an existing Angular app:
1. Run ng update #
The official migration path:
ng update @angular/cli @angular/core
The CLI handles the Angular dependency bumps and runs the v22 migrations — among them adding ChangeDetectionStrategy.Eager to existing components, adding withXhr() where the XHR backend is needed, and temporarily disabling the two new nullability diagnostics.
2. Review the OnPush and HTTP changes #
Find the ChangeDetectionStrategy.Eager lines the migration added and remove them component by component as you confirm each one works with OnPush. Then drop withFetch() from provideHttpClient() — Fetch is the default now.
3. Consider opportunistic Signal Forms migration #
For new forms, use form() from @angular/forms/signals. For existing forms, leave them; migrate when you next touch each form file.
What's next — roadmap signals #
The Angular team's official roadmap is not tied to specific versions, but it gives a few hints:
- Three framework goals — a better AI experience for developers, a better developer experience, and better framework performance
- AI tooling — deeper integrations with agentic tools, agent skills and new MCP features are on the list
- Web MCP — listed as "available to experiment with", matching its experimental status in v22
- Production ready — Signal Forms, the Resource API,
httpResource, zoneless change detection,linkedSignaland incremental hydration are all listed as production ready
For now, v22 is the version to be on.
What it means for your codebase #
If you're starting a new Angular project today: ship on v22. Use Signal Forms, signals everywhere, httpResource, the new template improvements. The framework's strongest endorsements are the right defaults.
If you're maintaining an existing app: ng update, smoke-test, ship the update. Most apps will see no behavior changes. Opportunistically adopt Signal Forms when you touch each form file. Reach for @angular/aria the next time you build a custom widget.
If you're on Angular ≤17: budget more time for the upgrade. The signal era (v17→v22) is a substantive change; running ng update between distant majors works but expect to address deprecations along the way.
Try it yourself #
The fastest way to feel v22 is to scaffold a fresh project:
npx -p @angular/cli@22 ng new hello-v22 --style=css --ssr=false
cd hello-v22
ng serve --open
Open app.ts. Notice: no NgModule, no BrowserModule, no zone.js reference, and the title is a signal(). Zoneless is the default for new projects, so app.config.ts has no change-detection provider at all — no provideZoneChangeDetection(), and no need for provideZonelessChangeDetection() (lesson 3.5).
If you wire up Claude Code with ng mcp enabled, you can ask:
onpush_zoneless_migrationThree steps, in order: (1) Run ng update @angular/cli @angular/core — handles the version bumps and the easy code transformations. (2) Run the signal-input/signal-query migrations: ng generate @angular/core:signal-input-migration and :signal-queries-migration. This converts @Input() decorators to input() signal functions and @ViewChild to viewChild() — purely mechanical, low risk. (3) Work through the ChangeDetectionStrategy.Eager markers that ng update added and move components to the new OnPush default one at a time — I can analyze each file for zone-dependent code first. After step 1 you’re on v22; after step 2 you’re using the modern signal APIs everywhere; after step 3 you’re getting the full benefit of the OnPush default.Angular v22 is the most polished release in recent memory. The signal era is no longer the future of the framework — it's the present. Upgrade when you can; the migration is straightforward, the wins are real, and v23 builds on this foundation directly.
Further reading #
- Angular v22 official announcement — the team's full post
- 10 Years of Angular: From Angular 2 to Signals and Zoneless — how v22 fits into a decade of Angular releases
- Angular Tutorial → Module 3: Signals & Reactivity — every signal primitive covered, including the v22 stabilizations
- Angular Tutorial → Module 4: Forms — Signal Forms, Reactive Forms, and migration paths
- Angular Tutorial → Module 8: CSR Performance — covers the v22-specific OnPush-default behavior and Web Vitals tactics
- Angular Tutorial → Module 12: AI × Angular —
ng mcpand AI-augmented Angular workflows
Go build something. v22 makes it easier than any prior release.
Up next in Angular
More from this topic
Enjoyed this article?
Get new Angular tutorials delivered. No spam — just code-first articles when they ship.
1 comment



Pingback: 10 Years of Angular: From Angular 2 to Signals and Zoneless - Code-JS