Angular 20 has just been released, introducing brand-new APIs, developer-experience enhancements, improved type checking, better CLI diagnostics, and the stabilization of features from earlier Angular versions. To help you navigate these changes, I’ve organized this article into four sections:
New Features: An overview of Angular 20’s new APIs
Developer Experience: Updates to the CLI, diagnostics, and error-catching tools
API Stability Changes: Which APIs are production-ready and which remain experimental
Breaking Changes: Key considerations when upgrading your project to Angular 20
New Features
An overview of the new APIs in Angular 20.
Supporting new features in templates
Angular 20 introduces several new features in the template compiler designed to enhance the developer experience and align with typescript expressions. The end goal is to have all Angular template expressions work exactly like TypeScript expressions. In future Angular releases, we can expect support for arrow functions and full alignment with the optional-chaining specification – see GitHub issue.
Here are the most notable updates introduced in Angular 20: template string literals, the exponentiation operator, the in keyword, and the void operator. Let’s explore each one.
Template literals
Previously, concatenating strings in Angular templates could be verbose. Now you can use clean, JavaScript-like template literals directly in your component templates.
Note that the angular template literals don’t work with inline-html written within typescript template literal. See this issue for more information.
Untagged Template Literals:
user-avatar.ts
@Component({
selector: 'app-user-avatar',
imports: [NgOptimizedImage],
templateUrl: './user-avatar.html',
})
export class UserAvatar {
readonly userId = input.required<string>();
}
user-avatar.html
<img
[ngSrc]="`https://i.pravatar.cc/150?u=${userId()}`"
width="100"
height="100"
/>
<p>{{ `User id: ${userId()}` }}</p>
Output:
Tagged Template Literals:
@Component({
selector: 'app-user-details',
template: '<p>{{ greet`Hello, ${name()}` }}</p>',
})
export class UserDetails {
readonly name = input<string>('John');
greet(strings: TemplateStringsArray, name: string) {
return strings[0] + name + strings[1] + '!';
}
}
Output:
This approach makes complex interpolations far more readable and maintainable.
Exponential Operator
Angular 20 adds support for the exponentiation operator (**) in templates, allowing you to raise numbers to a power without writing custom pipes.
For example:
@Component({
template: '{{2 ** 3}}'
})
export class AppComponent {}
This template will render 8, since 2 raised to the power of 3 equals 8.
In Keyword
The in operator lets you check whether an object contains a specific property before interpolating its value. This operator is useful for narrowing types or conditionally displaying properties in your components.
For example:
combat-logs.ts
@Component({
selector: 'app-combat-logs',
templateUrl: './combat-logs.html',
})
export class CombatLog {
readonly attacks = [
{ magicDamage: 10 },
{ physicalDamage: 10 },
{ magicDamage: 10, physicalDamage: 10 },
];
}
combat-logs.html
@for (attack of attacks; track attack) {
@let hasMagicDamage = 'magicDamage' in attack;
@if (hasMagicDamage) {
<p>{{ `Dealt ${attack.magicDamage} points of magic damage.` }}</p>
}
@let hasPhysicalDamage = 'physicalDamage' in attack;
@if (hasPhysicalDamage) {
<p>{{ `Dealt ${attack.physicalDamage} points of physical damage.` }}</p>
}
}
Output:
Void Operator
The final new operator is void. Use it to explicitly ignore the return value of a bound listener, preventing unintentional calls to event.preventDefault() if your handler returns false.
For example:
@Directive({
host: { '(mousedown)': 'void handleMousedown()' },
})
export class MouseDownDirective {
handleMousedown(): boolean {
// Business logic...
return false;
}
}
Asynchronous Redirect Function
Angular now supports asynchronous redirect functions. The redirectTo property can return a Promise, an Observable of string | UrlTree. This change allows you to build redirect logic that waits for data before deciding where to send the user.
For Example:
export const ROUTES: Routes = [
…,
{
path: '**',
redirectTo: () => {
const router = inject(Router);
const authService = inject(AuthService);
return authService.isAuthorized$.pipe(
map((isAuthorized) =>
router.createUrlTree([`/${isAuthorized ? 'home' : 'login'}`]),
),
);
},
},
];
Abort redirection
Angular 20 adds a new method Router.getCurrentNavigation()?.abort(), allowing developers to cancel ongoing navigations. This enables better integration with the browser’s Navigation API – for example, stopping a route change when a user clicks the browser’s Stop button.
New Features of NgComponentOutlet
NgComponentOutlet provides a dynamic way to instantiate and render components within templates. It works like RouterOutlet but doesn’t require router configuration, making it ideal for scenarios with dynamically loaded components. Although powerful, it previously required a lot of manual setup, which could be cumbersome.
NgComponentOutlet before Angular 20:
@Component({
template: `<ng-container #container />`
})
export class AppComponent {
private _cmpRef?: ComponentRef<MyComponent>;
private readonly _container = viewChild('container', {
read: ViewContainerRef
});
createComponent(title: string): void {
this.destroyComponent(); // Otherwise it would create second instance
this._cmpRef = this._container()?.createComponent(MyComponent);
this._cmpRef?.setInput('title', title);
}
destroyComponent(): void {
this._container()?.clear();
}
}
In Angular 20 there’s a new API for NgComponentOutlet, making it easier to use by handling manual configuration internally. From now ngComponentOutlet directive has following inputs:
- The ngComponentOutlet is used to specify the component type.
- The ngComponentOutletInputs lets you pass input values straight to the component.
- The ngComponentOutletContent is used to define the content nodes for content projection.
- The ngComponentOutletInjector passes a custom injector to the component that’s created dynamically.
This API enhancement makes dynamic component code far more readable.
New NgComponentOutlet API in Angular 20+:
@Component({
selector: 'app-root',
imports: [NgComponentOutlet],
template: `
<ng-container
[ngComponentOutlet]="myComponent"
[ngComponentOutletInputs]="myComponentInput()"
[ngComponentOutletContent]="contentNodes()"
[ngComponentOutletInjector]="myInjector"
#outlet="ngComponentOutlet"
/>
<ng-template #emptyState>
<p>Empty State</p>
</ng-template>
<button (click)="createComponent()">Create Component</button>
<button (click)="destroyComponent()">Destroy Component</button>
`,
})
export class App {
private readonly _vcr = inject(ViewContainerRef);
private readonly _injector = inject(Injector);
protected myComponent: Type<DynamicComponent> | null = null;
protected readonly myComponentInput = signal({ title: 'Example Title' });
private readonly _emptyStateTemplate =
viewChild<TemplateRef<unknown>>('emptyState');
readonly contentNodes = computed(() => {
if (!this._emptyStateTemplate()) return [];
return [
this._vcr.createEmbeddedView(this._emptyStateTemplate()!).rootNodes,
];
});
readonly myInjector = Injector.create({
providers: [{ provide: MyService, deps: [] }],
parent: this._injector,
});
createComponent(): void {
this.myComponent = DynamicComponent;
}
destroyComponent(): void {
this.myComponent = null;
}
}
Input, Output Bindings and directives support for dynamically created components
Angular now lets you apply inputs, outputs, two-way bindings, and host directives directly when creating dynamic components.
By using helper functions like inputBinding, twoWayBinding, and outputBinding alongside a directives array, you can instantiate a component with template-like bindings and attached directives in a single call to ViewContainerRef.createComponent.
This change makes dynamic components API much more powerful.
For Example:
@Component({
...
})
export class AppWarningComponent {
readonly canClose = input.required<boolean>();
readonly isExpanded = model<boolean>();
readonly close = output<boolean>();
}
@Component({
template: ` <ng-container #container></ng-container> `,
})
export class AppComponent {
readonly vcr = viewChild.required('container', { read: ViewContainerRef });
readonly canClose = signal(true)
readonly isExpanded = signal(true)
createWarningComponent(): void {
this.vcr().createComponent(AppWarningComponent, {
bindings: [
inputBinding('canClose', this.canClose),
twoWayBinding('isExpanded', this.isExpanded),
outputBinding<boolean>('close', (isConfirmed) => console.log(isConfirmed))
],
directives: [
FocusTrap,
{
type: ThemeDirective,
bindings: [inputBinding('theme', () => 'warning')]
}
]
})
}
}
Expose Injector.destroy on Injector created with Injector.create
In Angular 20, destroy() was exposed on injectors created via Injector.create(), letting you explicitly tear down user-owned injectors:
const injector = Injector.create({
providers: [{ provide: LOCALE_ID, useValue: 'en-US' }],
});
// API exposed in Angular 20
injector.destroy();
Add keepalive support for fetch requests
Angular now supports the Fetch API’s keepalive flag in HttpClient requests, allowing you to perform asynchronous operations during page unload events (e.g., sending analytics data).
By enabling { keepalive: true } in your fetch-based calls, Angular lets these requests run to completion even during page unload events.
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
private readonly _http = inject(HttpClient);
sendAnalyticsData(data: AnalyticsData): Observable<unknown> {
return this._http.post('/api/analytics', data, { keepalive: true });
}
}
Scroll Options in ViewportScroller
Angular’s ViewportScroller now accepts ScrollOptions in its scrolling methods – scrollToAnchor and scrollToPosition.
Signal Forms and Selectorless Components
In Angular 20, we will not receive:
- Signal Forms – a new signal-based pattern for building forms.
- Selectorless Components – Update to the way we use components and directives in Angular HTML templates.
Both of these major features are still under development and not yet released.
Developer Experience
What’s changed in the CLI, diagnostics, and error-catching tooling.
Type-Checking Support for Host Bindings
Angular 20 adds type checking for host bindings. Every expression in a component’s or directive’s host metadata or any function annotated with @HostBinding or @HostListener is now validated.
The Angular Language Service can now:
- Show hover-tooltips with the types of bound variables or functions
- Automatically update host-binding references when you rename a variable or method
This feature will significantly reduce runtime errors and make refactoring host bindings a pleasure.
Extended Diagnostic for Invalid Nullish Coalescing
In TypeScript, mixing the nullish coalescing operator (??) with logical OR (||) or logical AND (&&) without parentheses is a compile-time error. Previously, Angular templates allowed this without any warning.
Starting in Angular 20, the compiler now provides a diagnostic when you mix these operators without parentheses, and suggests adding the necessary grouping. For example:
@Component({
template: `
<button [disabled]="hasPermission() && (task()?.disabled ?? true)">
Run
</button>
`,
})
class MyComponent {
hasPermission = input(false);
task = input<Task|undefined>(undefined);
}
Extended Diagnostic for Uninvoked Track Functions
When migrating from *ngFor to the control-flow @for, passing a track function without invoking it (e.g. track trackByName) causes the list to be recreated on every change.
Because of that in Angular 20, a new diagnostic alerts you whenever you forget to call the track function in an @for block. This change will help you in maintaining optimal performance.
Incorrect (triggers warning):
@for (item of items; track trackByName) {}
Correct (no warning):
@for (item of items; track trackByName(item)) {}
Missing Structural Directives Import Detection
Prior to Angular 20, the compiler would flag missing imports for built-in structural directives (like *ngIf or *ngFor), but it did not suggest importing custom structural directives. That could cause problems – especially when migrating to standalone components. In such cases it’s easy to overlook an import.
Angular 20 now warns you if you use a custom structural directive without importing it.
For Example:
@Component({
selector: 'app-root',
template: `<div *appFeatureFlag="true"></div>`,
})
export class App {}
In this case, you’ll see the following warning:
API Stability Changes
Signal related APIs
With Angular 20, the team continues to mature its signal-based APIs:
toSignal and toObservable
These conversion utilities are now stable, so you can freely bridge between signals and observables in production without worrying about breaking changes in future releases.
linkedSignal
This API – used to create a writable signal that derives its value from another signal – has also been promoted to stable in v20.
effect
The effect API, which lets you run side-effect logic whenever a signal changes, is now stable. It went through several tweaks during its developer-preview stage, so its stabilization is a key milestone.
afterRender → afterEveryRender
To make its purpose clearer, the former afterRender hook has been renamed to afterEveryRender. In Angular 20, both afterEveryRender and its companion afterNextRender are officially stable.
Next Step Towards Zoneless Angular
Removing zone.js has been a major focus for the Angular team over the past year, and they’ve made significant progress. In Angular 20, the API for zoneless change detection has moved from experimental to developer preview.
As part of this update, the provider name changed from
provideExperimentalZonelessChangeDetection
to
provideZonelessChangeDetection
Several community members report that switching to zoneless change detection has boosted their Lighthouse scores by a few points. Google is also rolling it out in more of their applications – for example, the Google Fonts app has been running without zone.js for seven months at the time of writing.
When creating new Angular applications using ng new with Angular CLI version 20, we’ll also be prompted to choose whether we want to create a zoneless application.
If you’re planning to migrate your app to zoneless change detection, check out the former provideExperimentalCheckNoChangesForDebug function. In Angular 20, it’s now called provideCheckNoChangesConfig and is also in developer preview. This provider helps you spot any updates that didn’t trigger change detection – an excellent way to verify your application’s readiness for a zoneless change detection.
Pending Tasks in Angular SSR
If you’re using server-side rendering (SSR), you’ll be glad to know that the PendingTasks API is now stable in Angular 20. This API lets you manage your application’s stability by delaying the SSR response until specified tasks have completed.
At the same time, the custom RxJS operator pendingUntilEvent – which relies on PendingTasks under the hood – has been promoted from experimental to developer preview.
Breaking Changes
Things to watch out for when you upgrade your project to Angular 20.
Angular Peer Dependencies
In Angular 20, the required peer dependencies are:
- Node: ^20.11.1 || ^22.11.0 || ^24.0.0
- TypeScript: >=5.8.0 <5.9.0
- RxJs: ^6.5.3 || ^7.4.0
Support for Node 18 and TypeScript versions below 5.8 has been dropped.
Ng-Reflect Attributes
Since Angular 20, the framework no longer emits ng-reflect-* attributes in development mode. After upgrading, any tests that rely on those attributes will start failing. To visualize the impact in DOM let’s take a look at app-child component.
@Component({
selector: 'app-child',
template: ` <h2>Child Component property: {{ property() }}</h2> `,
})
export class AppChildComponent {
readonly property = input('');
}
Before upgrade DOM looked like (in dev mode):
After upgrade to Angular 20 DOM will look like (in dev mode):
If you need to restore ng-reflect-* attributes temporarily, you can add the provideNgReflectAttributes() provider in your main app provider. However, I strongly recommend refactoring your tests to use stable, custom attributes – such as data-cy or data-test-id – which won’t break when Angular internals change.
InjectFlags Removal
In Angular 20, the previously deprecated InjectFlags API has been removed. To help with the transition, Angular provides a migration schematic.
Schematic will automatically convert your code from:
import { inject, InjectFlags, Directive, ElementRef } from '@angular/core';
@Directive()
export class Dir {
el = inject(ElementRef, InjectFlags.Optional | InjectFlags.Host | InjectFlags.SkipSelf);
}
to the options-object syntax:
import { inject, Directive, ElementRef } from '@angular/core';
@Directive()
export class Dir {
el = inject(ElementRef, { optional: true, host: true, skipSelf: true });
}
Hammer JS Deprecation
Official support for HammerJS is deprecated and will be removed in a future major release – angular 21. Plan for your own custom implementation if you rely on touch gestures and hammer js in your application.
Conclusions
Angular 20 delivers many new template capabilities, stabilizes key signal-rxjs based APIs, and strengthens the developer experience with enhanced diagnostics and CLI improvements.
It also marks a significant step toward a zoneless future and tighter SSR integration. While signal-driven forms and selectorless components are still in progress, Angular 20 offers a smooth upgrade.
Don’t miss the minor updates that rolled out between Angular 19 and 20:
- Angular 19.1 – HMR for templates, subPath in Multilingual Applications and more
- Angular 19.2 – experimental httpResource, untagged template literals in expressions and more
What’s your favorite Angular 20 feature? Share your thoughts below!