Announcing Angular v20
The past couple of years have been transformative for Angular, as we’ve unleashed major advancements like reactivity with Signals and the power of Zoneless applications. We hope these features have helped the Angular community build the next generation of web applications with fast time-to-market and robust performance.
And we are just getting started! Angular v20 is our latest release where we have spent countless hours polishing some of our in-progress features for the rock-solid developer experience that you deserve.
Some of the highlights:
- Stabilizing APIs such as
effect
,linkedSignal
,toSignal
, incremental hydration, route-level render mode config and promoting zoneless to developer preview - Improved debugging with Angular DevTools and partnering with Chrome for custom Angular reporting directly in Chrome DevTools
- Polishing developer experience with style guide updates, type checking and language service support for host bindings, support for untagged template literal expressions in templates, template hot module replacement by default, and more.
- Advancements in GenAI development with llms.txt and angular.dev guides and videos for building Generative AI applications
- Launching a request for comments for an official mascot for Angular
Promoting reactivity features to stable
We spent the past three years rethinking Angular’s reactivity model to make it more robust and future-proof. In Angular v16 we shipped a developer preview of Angular Signals and since then, they got a wide adoption in and outside of Google.
YouTube shared on stage how using Angular Signals with Wiz, they improved input latency in Living Room with 35%. In the meantime, TC39 kicked off an investigation to introduce Signals to the JavaScript language with reference implementation based on Angular Signals.
After collecting feedback from RFCs and iterating on the implementation, we promoted signal
, computed
, input
and view queries APIs to stable. Today, we are announcing effect
, linkedSignal
and toSignal
as stable as well.
New, experimental APIs
To tackle managing asynchronous state with Angular, in v19 we developed the resource API. Since then, we introduced resource streaming and created a new API called httpResource
that allows you to make HTTP requests with a Signal-based reactive API. Both of these APIs are available as part of v20 as experimental.
The resource
API allows you to initiate an asynchronous action when a signal changes and expose the result of this action as a signal:
const userId: Signal<string> = getUserId();
const userResource = resource({
params: () => ({id: userId()}),
loader: ({request, abortSignal}): Promise<User> => {
// fetch cancels any outstanding HTTP requests when the given `AbortSignal`
// indicates that the request has been aborted.
return fetch(`users/${request.id}`, {signal: abortSignal});
},
});
The code above will fetch the user with the particular identifier, when the userId
signal changes.
Now let’s suppose we’re getting data from a WebSocket. For this purpose we can use a streaming resource:
@Component({
template: `{{ dataStream.value() }}`
})
export class App {
// WebSocket initialization logic will live here...
// ...
// Initialization of the streaming resource
dataStream = resource({
stream: () => {
return new Promise<Signal<ResourceStreamItem<string[]>>>((resolve) => {
const resourceResult = signal<{ value: string[] }>({
value: [],
});
this.socket.onmessage = event => {
resourceResult.update(current => ({
value: [...current.value, event.data]
});
};
resolve(resourceResult);
});
},
});
}
In this minimal example, we declare a new streaming resource, which returns a promise of a signal. The signal has a value type ResourceStreamItem<string[]>
, which means that the signal can hold the value { value: string[] }
or {error: … }
in case we want to return an error.
We emit the values we receive over the WebSocket via the resourceResult
signal.
Building on top of this pattern, we also shipped the experimental httpResource
:
@Component({
template: `{{ userResource.value() | json }}`
})
class UserProfile {
userId = signal(1);
userResource = httpResource<User>(() =>
`https://example.com/v1/users/${this.userId()}`
});
}
The snippet above will send an HTTP GET request to the URL we specified every time when the userId
changes. httpResource
returns HttpResourceRef
which has a value
property of type signal that we can directly access in the template. The userResource
has other values, such as isLoading
, headers
, and others, as well.
Under the hood, httpResource
uses HttpClient
so you can specify interceptors in the HttpClient
provider:
bootstrapApplication(AppComponent, {providers: [
provideHttpClient(
withInterceptors([loggingInterceptor, cachingInterceptor]),
)
]});
Promoting Zoneless to developer preview
Over the past six months, we made a lot of progress in zoneless, specifically around server-side rendering and error handling.
Many developers use Zone.js for capturing errors in their apps even without realizing it. Zone.js also lets the framework know when we’re ready to flush the server-side rendered application to the client. In the world of zoneless, we had to find robust solutions for these problems.
In v20 we now have a default handler for unhandledRejection
and uncaughtException
in Node.js during SSR to prevent the node server from crashing in case of errors.
On the client, you can include provideBrowserGlobalErrorListeners
in your providers. You can start using zoneless today by updating your list of providers:
bootstrapApplication(AppComponent, {providers: [
provideZonelessChangeDetection(),
provideBrowserGlobalErrorListeners()
]});
In addition, make sure you remove the zone.js polyfill from your angular.json
. Learn more about the benefits of zoneless and how to transition your project in our documentation.
If you’re creating a new Angular project, you can get it to be zoneless from the start using the CLI:
Solidifying Angular on the server
In v20 we also focused on polishing our flagship server-side rendering features — incremental hydration and route-level rendering mode configuration. Today, we’re happy to promote both of them to stable!
As a reminder, incremental hydration makes your apps faster by downloading and hydrating a portion of the page on a particular trigger. This way, your users don’t have to download all the JavaScript associated with a particular page, instead they can gradually download only the parts that they need.
To start using incremental hydration today, configure hydration by specifying withIncrementalHydration
:
import { provideClientHydration, withIncrementalHydration } from '@angular/platform-browser';
// ...
provideClientHydration(withIncrementalHydration());
In the templates of your components now you can use deferrable views:
@defer (hydrate on viewport) {
<shopping-cart/>
}
That way, Angular will download the shopping cart component together with its transitive dependencies and hydrate this part of the UI only when it enters the viewport.
Additionally, you can now use route-level rendering mode configuration as a stable API! If different routes in your app have different rendering requirements, you can configure that in a server-route configuration:
export const routeConfig: ServerRoute = [
{ path: '/login', mode: RenderMode.Server },
{ path: '/dashboard', mode: RenderMode.Client },
{
path: '/product/:id',
mode: RenderMode.Prerender,
async getPrerenderParams() {
const dataService = inject(ProductService);
const ids = await dataService.getIds(); // ["1", "2", "3"]
// `id` is used in place of `:id` in the route path.
return ids.map(id => ({ id }));
}
}
];
In the snippet above we configure to render the login page on the server, the dashboard on the client, and prerender the product pages.
Notice that the product page requires an id parameter. To resolve the identifiers for each product, we can use the asynchronous function getPrerenderParams
. It returns an object in which its keys map to router parameters. In the case of the /product/:id
page we return an object with an id
property.
You can host your server-side rendered apps with most cloud providers. We partnered closely with Firebase App Hosting on a seamless deployment story that supports hybrid rendering (SSR, SSG, and CSR) and provides you the security and scalability of Google Cloud.
Polishing developer experience
We spent a lot of time while developing v20 on engineering excellence — polishing existing APIs to improve your developer experience. We did this across the board — framework, router, forms, http, etc. Let me share more about the work we did here!
Performance insights in Chrome DevTools
To further enhance the developer experience and provide deeper insights into application performance, we’ve collaborated with the Chrome DevTools team to integrate Angular-specific profiling data directly into the Performance panel. Previously, developers often had to switch between framework-specific profilers and the browser’s DevTools, making it challenging to correlate information and pinpoint bottlenecks, especially with minified production code. This new integration aims to solve that by displaying Angular runtime data, such as component rendering, change detection cycles, and event listener execution, within the same timeline as other browser performance metrics.
This direct integration, available starting in Angular v20, leverages the Performance panel extensibility API, specifically using the console.timeStamp
API for its low overhead, ensuring that profiling doesn’t negatively impact application performance. Developers can now gain enhanced visibility into Angular’s internal workings, with color-coded entries to distinguish between developer-authored TypeScript code and Angular compiler-generated code. To enable this feature, simply run the global utility ng.enableProfiling()
in your application or the DevTools console. This advancement provides a more intuitive and comprehensive performance analysis experience, empowering developers to build even more performant Angular applications.
On the screenshot above, you can see this feature in action. Notice how at the bottom of the performance timeline there’s a track dedicated to Angular. With the color-coded bars, you can preview component instantiation, running change detection, etc. Both Angular DevTools and the Angular track in the Chrome performance timeline use the same hooks with the difference that Chrome’s performance timeline can put your app’s lifecycle into the context of other JavaScript calls outside the framework.
In addition, the Angular track in Chrome’s performance timeline shows some data that’s currently not present in Angular DevTools, such as component and provider instantiation.
Framework additions and improvements
To dynamically create an Angular component you can use the createComponent
function. In v20 we introduce new features that let you apply directives and specify bindings to dynamically created components:
import {createComponent, signal, inputBinding, outputBinding} from '@angular/core';
const canClose = signal(false);
const title = signal('My dialog title');
// Create MyDialog
createComponent(MyDialog, {
bindings: [
// Bind a signal to the `canClose` input.
inputBinding('canClose', canClose),
// Listen for the `onClose` event specifically on the dialog.
outputBinding<Result>('onClose', result => console.log(result)),
// Creates two way binding with the title property
twoWayBinding('title', title),
],
directives: [
// Apply the `FocusTrap` directive to `MyDialog` without any bindings.
FocusTrap,
// Apply the `HasColor` directive to `MyDialog` and bind the `red` value to its `color` input.
// The callback to `inputBinding` is invoked on each change detection.
{
type: HasColor,
bindings: [inputBinding('color', () => 'red')]
}
]
});
Above we create a dialog component and specify:
canClose
input binding, passing the signalcanClose
as a value- Set the output
onClose
to a callback that logs the emitted result - Two way binding between the
title
property and thetitle
signal
Additionally, we add the FocusTrap
and HasColor
directives to the component. Notice that we can also specify input bindings for the HasColor
directive that we apply to MyDialog
.
Extended template expression syntax
We’ve been bridging the gap between Angular template expressions and full JavaScript syntax to enable higher expressiveness and better developer experience. Today, we’re introducing support for the exponential operator **
and in
operator:
<!-- n on power two -->
{{ n ** 2 }}
<!-- checks if the person object contains the name property -->
{{ name in person }}
In v20 we also enable you to use untagged template literals directly in expressions:
<div [class]="`layout col-${colWidth}`"></div>
Extended diagnostics
To guard against common errors, we introduced static checks that detect invalid nullish coalescing, detection of missing imports for structural directives, and a warning when you don’t invoke the track
function you’ve passed to @for
:
@Component({
template: `
@for (user of users; track trackFn) {
<!-- ... ->
}
`
})
class UserList {
users = getUsers();
trackFn() {
// ... body
}
}
The @for
loop in Angular templates accepts a track expression. In practice, trackFn
by itself is an expression which returns the trackFn
function which is a valid value. At the same time, most likely we’d have wanted to call trackFn
and the new diagnostics makes it easier to catch such mistakes.
Style guide updates
After seeing how thousands of apps use Angular over the past decade we decided to update our style guide. Our main goals are to modernize it and remove the unnecessary complexities.
After we collected feedback from an RFC, we introduced a series of simplifications — removing non-Angular specific code health practices from the style guide and moving Angular best practices that are unrelated to coding style to the documentation. We also made filename and class name suffixes optional to encourage more intentional naming of the abstractions which reduces boilerplate.
Starting in Angular v20, by default Angular CLI will not generate suffixes for your components, directives, services, and pipes. For existing projects, ng update
will enable suffix generation by updating your angular.json
. To enable suffix generation in new projects, use the following schematic configuration:
{
"projects": {
"app": {
...
"schematics": {
"@schematics/angular:component": { "type": "component" },
"@schematics/angular:directive": { "type": "directive" },
"@schematics/angular:service": { "type": "service" },
"@schematics/angular:guard": { "typeSeparator": "." },
"@schematics/angular:interceptor": { "typeSeparator": "." },
"@schematics/angular:module": { "typeSeparator": "." },
"@schematics/angular:pipe": { "typeSeparator": "." },
"@schematics/angular:resolver": { "typeSeparator": "." }
},
...
}
Angular has evolved a lot over the years and we wanted to reflect its evolution in the style guide as well. As a result, we removed most guidance related to NgModules and revisited the usage of @HostBinding
and @HostListener
in favor of the host
object within the directive metadata. To ensure we don’t regress developer experience with the new guidelines, we also addressed a couple of gaps in the host binding support.
Improved host bindings
A reason why historically we’ve been recommending @HostBinding
and @HostListener
was that they had marginally better editor support than the host
object in component metadata since you can use them right on a specific binding or a method. At the same time, they could be hard to spot, use decorators, and could lead to more cumbersome code.
In Angular v20 we’re introducing type checking and language support for host binding and listener expressions.
In the gif below you can see this feature in action. We first get an error because we call a function named getAppTile
instead of getAppTitle
. Once we fix this problem, the language service detects that the program doesn’t type check since we’re passing an argument to a function that doesn’t expect any arguments.
To enable this feature, set the typeCheckHostBindings
property under angularCompilerOptions
in tsconfig.json
to true
. We’ll enable this feature by default in Angular v21.
Incremental hydration in Angular DevTools
To simplify debugging of incremental hydration and deferrable views, you can now preview them in Angular DevTools directly!
The screen capture below shows how you can inspect a defer block and the content that it later loads.
When using defer blocks with incremental hydration, you’d also get icons indicating whether Angular hydrated the current component yet.
Experimental support for vitest
With the deprecation of Karma, we worked with testing framework authors to find a well maintained replacement that enables browser testing. We landed a pull request that creates an experimental playground for us to try different test runners.
In v20, Angular CLI comes in with an experimental vitest support that has watch mode and browser testing!
To give vitest a try in node environment, within your project run:
npm i vitest jsdom --save-dev
After that update your testing configuration in angular.json to:
"test": {
"builder": "@angular/build:unit-test",
"options": {
"tsConfig": "tsconfig.spec.json",
"buildTarget": "::development",
"runner": "vitest"
}
}
Next you may have to update your unit test files to include correct imports:
...
import { describe, beforeEach, it, expect } from 'vitest';
...
Finally, ng test to run your unit tests with vitest.
Quality of life improvements in Angular Material
In this release we polished further our button component to better align with the M3 specification.
Tonal button
A few of the changes:
- We implemented tonal button
- Aligned terminology with M3 specification
- Added the ability to set the default appearance for buttons
- Added
matIconButton
selector to the icon button for consistency
A few quality of life improvements that we implemented include:
- New
closePredicate
for dialog which closes a request with 108 👍 - New overlay APIs for better tree-shaking
- Now handling `prefers-reduced-motion` automatically
- New DI token to disable animations
MatButton
andMatAnchor
are combined so users don’t have to import both of them.
Supporting developers utilizing GenAI
To enable LLMs produce modern Angular code and enable you to build apps with GenAI we kicked off two efforts
- Maintaining an
llms.txt
file (see the pull request on GitHub) that helps large language models discover the latest Angular documentation and code samples - Providing starting point for developers who are building apps using GenAI
Some language models still produce older Angular syntax using structural directives instead of the latest control flow, or using NgModules instead of standalone components, directives, and pipes. Resolving this problem is a multi step process that we started with creating an llms.txt
file. In the future, we’ll continue providing code samples using the latest Angular syntax and explore the development of system prompts that hint LLMs to use the correct APIs.
The second effort we started is to provide guidelines for developers who are building APIs with AI features. We ran multiple live streams showcasing how you can leverage Genkit and Vertex AI in your Angular app. We open sourced the sample apps and listed some of the best practices we discovered on angular.dev/ai.
That’s just the very beginning of making Angular the solution for your agentic AI apps.
Deprecation of NgIf, NgFor, and NgSwitch
We introduced Angular’s built-in control flow back in v17 to bring a series of improvements:
- More intuitive syntax, that’s closer to JavaScript
- Simpler use, without need to import another module or individual directives
- Performance improvement by updating the diffing algorithm
- Improved type checking via type narrowing
We also shipped a schematic that with a single line of code allows you to move your projects from structural directives to the built-in control flow:
ng generate @angular/core:control-flow
Currently, more than half of the Angular v17+ apps on the HTTP Archive public data set use the new syntax!
Based on the community sentiment and adoption metrics, moving forward we are deprecating *ngIf
, *ngFor
, and *ngSwitch
and encouraging everyone to use the latest built-in control flow. This means that under our deprecation policy we can remove structural directives in version 22, one year from now.
Official Angular mascot
As Angular continues to grow and evolve, we’re excited to announce a new initiative that will further enrich our amazing community: the creation of an official Angular mascot! While Angular is a well-recognized and widely adopted framework, it has been missing a fun, visual representation that so many other successful open-source projects enjoy. We’ve heard your requests for something tangible to connect with, like a plushy or keychain, and we’re thrilled to embark on this creative journey with all of you. Our team collaborated with the designers behind the Dart and Firebase mascots, sharing Angular’s core strengths and community spirit. This process led to three initial mascot proposals that we’re eager to share.
This is where you, the Angular community, come in! True to Angular’s values of inclusivity and community-driven decisions, we’re opening up the process for everyone to contribute. Find the official Angular mascot RFC at goo.gle/angular-mascot-rfc.
Here’s a sneak peak of the initial concepts:
“Angular shaped character” drawing inspiration from our logo
Wise and resilient “Anglerfish” (much cuter than its real-life counterpart!)
A variation of the Anglerfish.
We invite you to check out the full RFC, cast your vote for one of these proposals, suggest improvements, and even propose names. Your feedback has been invaluable in shaping Angular’s features, and we’re excited to see that same collaborative spirit define our future mascot. Let’s create something truly special together!
Thank you for all the contributions!
Around the world there are thousands of library authors, conference and meetup organizers, educators, and other people who are helping move the web forward through Angular! We’d never been here without you all.
Since the release date of v19, we received and merged commits from over 225 people across the framework, components, and CLI! Every single change that you made helps us make Angular better. I wanted to highlight some of the features we got from community members:
- Domenico Gemoli added
markAllAsDirty
toAbstractControl
which now makes a specific component and all of its children as dirty - Enea Jahollari implemented extended diagnostic for uninvoked track function on
@for
- Enea Jahollari also added a migration to convert templates to use self-closing tags for code consistency
- Jeri Peier made it possible to use form validators with sets
- Kevin Brey worked on extended diagnostics to help you detect missing structural directive imports
- Lukas Spirig introduced support for custom elements for RouterLink
- Meddah Abdallah introduced asynchronous redirects to the router
- Younes Jaaidi made it possible to run tests with ahead of time compilation in Jest and Karma
Thank you all 🙏
Ahead to the future!
As part of v20 we shipped a lot of polishing touches to the large efforts we kicked off over the past couple of years such as reactivity, zoneless, incremental hydration, framework and forms APIs. We also put initial high-level sketches for upcoming advancements such as selectorless, signal-forms, unit testing, and an official Angular mascot!
We’re building Angular for you and your input is critical in how we move forward with any of these initiatives. As our high-level plans for these major projects take shape, we’ll share updates and requests for comments. Until now, make sure you share your thoughts for the future, official Angular mascot! We’re very excited to find an identity that symbolizes Angular’s product and values. Let us know what you think about the concepts in the RFC!
Until next time and thank you for being part of Angular’s journey 🙏