What's new in Angular 21.1?
Angular 21.1.0 is here!
Two months after the huge v21 release, the Angular team delivers a new minor update.
Let's dive in!
Signal Forms
A meaningful change in Signal Forms is the renaming of the [field] directive to [formField].
The [field] selector was too generic and likely to cause naming collisions with existing components.
Therefore, the Angular team decided to rename both the directive class (from Field to FormField)
and the directive selector (from [field] to [formField]).
You should now use [formField] instead of [field] to bind form controls:
<input [formField]="form.name" />
The Field directive has been removed,
so you'll have some renaming to do if you already use experimental Signal Forms.
Another name change in Signal Forms is the renaming of the field property to fieldTree
in validation error interfaces and field contexts.
If you're writing custom validators,
you'll need to update your code to use fieldTree instead of field:
// Before
const error: ValidationError = {
// 👇 targets the password field
field: context.fieldTree.password,
kind: 'password-different-from-email',
message: 'Password should not be the same as email'
};
// After v21.1
const error: ValidationError = {
// 👇 targets the password field
fieldTree: context.fieldTree.password,
kind: 'password-different-from-email',
message: 'Password should not be the same as email'
}
This change affects the ValidationError interface and the FieldContext interface.
Signal Forms gained a new feature in v21.1: the ability to automatically apply CSS classes to fields based on their state 🚀.
One of the missing features of signal forms compared to the classic reactive/template-driven forms
was the automatic CSS classes that were added to form controls
(ng-valid, ng-dirty, ng-touched, etc.).
This is now possible using the provideSignalFormsConfig() function
in your application configuration:
provideSignalFormsConfig({
classes: {
'is-invalid': field => field.state().invalid() && field.state().touched()
}
})
In this example, the is-invalid CSS class will be automatically added
to any field that is invalid and touched.
You can define as many classes as you want,
and use any property of the Field/FormField directive,
like its state (invalid(), touched(), dirty(), etc.)
or its host element (a new property added in this version)
to determine when the class should be applied.
This makes it easier to style your forms, especially when using CSS frameworks like Bootstrap or Tailwind that rely on specific CSS classes to style form inputs.
Note that you can have the "old" behavior with ng-valid/ng-invalid/ng-dirty/ng-touched... classes
by using:
provideSignalFormsConfig({
classes: NG_STATUS_CLASSES,
});
Signal Forms now also support custom control directives!
Previously, the [field] binding could only be used with components.
Now, you can create directives that implement the FormValueControl or FormCheckboxControl interfaces
and bind them with [formField].
<input [formField]="form.name" appCustomControl />
Signal Forms also gained a new focusBoundControl()
method on the field state.
This is particularly useful for accessibility
or when you want to focus a specific field after a validation error.
This allows you to programmatically focus the input element associated with a form field, for example when a submission failed:
protected async register(event: SubmitEvent) {
event.preventDefault();
await submit(this.userForm, async form => {
// ...
});
// 👇 automatically focus the first field with an error
const firstError = this.userForm().errorSummary()[0];
if (firstError?.fieldTree) {
firstError.fieldTree().focusBoundControl();
}
This will automatically focus the first input/select/textarea
associated with a field that has an error,
but the cool thing is that it can also work with custom control components!
You just have to implement a focus() method in your custom control,
and focusBoundControl() will automatically call it:
@Component({
// ...
})
export class BirthYearInput implements FormValueControl<number | null> {
// ...
// 👇
focus() {
this.birthYearInput().nativeElement.focus();
}
NOTE: We added a Signal Forms exercise to our online workshop, check it out!
Control flow
The @switch control flow now supports multiple consecutive @case blocks matching a single content block.
Previously, each @case required its own content.
Now you can specify multiple conditions for a single block,
similar to fall-through behavior in traditional switch statements:
@switch (status) {
@case ('draft')
@case ('pending') {
<p>Your document is not yet published</p>
}
@case ('published') {
<p>Your document is live</p>
}
@default {
<p>Unknown status</p>
}
}
In this example, both 'draft' and 'pending' statuses will display the same message, making the code more concise when multiple conditions should produce the same result.
Template spread operator
Angular templates now support the spread operator (...)!
This allows you to spread an object into an object
or an array into another array:
@let users = [currentUser, ...admins];
It also supports the spread syntax in function calls. This is particularly useful for functions that use rest parameters, a syntax that allows a function to accept an indefinite number of arguments as an array:
<button (click)="sum(...counters)">Sum all</button>
Spoiler for the next version: we will get arrow function support in templates in v21.2!
Router
The router introduces a new standalone isActive() function that returns a computed signal
indicating whether a given URL is currently active.
This new function is a more tree-shakeable alternative to the existing Router.isActive() method,
which is now deprecated in v21.1:
import { isActive } from '@angular/router';
const active = isActive(
'/home',
this.router,
{ paths: 'exact', queryParams: 'exact', fragment: 'ignored', matrixParams: 'ignored' }
);
The function returns a signal that automatically updates when the router state changes, making it easier to reactively track active routes.
The router also gained better memory management capabilities in v21.1
if you use a custom RouteReuseStrategy implementation
(which is not that common).
A new experimental feature withExperimentalAutoCleanupInjectors() automatically destroys
unused route injectors after navigation,
helping to prevent memory leaks in applications with many routes or long-lived sessions.
You can enable it when configuring your routes:
provideRouter(routes, withExperimentalAutoCleanupInjectors())
Additionally, a new destroyDetachedRouteHandle() function is available
for manually cleaning up detached route handles in custom RouteReuseStrategy implementations.
Finally, the router introduces an experimental integration with the browser's Navigation API. This API is a modern alternative to the traditional History API, providing a more robust way to handle navigations.
By enabling this experimental feature, the Angular router can:
- intercept navigations triggered outside the router and convert them to SPA navigations.
- leverage native browser scroll and focus restoration.
- communicate ongoing navigations to the browser for better accessibility and user experience (native loading progress, stop button, etc.).
You can enable it using the withExperimentalPlatformNavigation() feature:
provideRouter(routes, withExperimentalPlatformNavigation())
This feature is highly experimental
and the native browser support is currently very limited.
This won't be stabilized until the Navigation API is more widely supported.
When this is done, it may not become a router feature (with...)
but rather a different router provider,
which would allow to tree-shake the current history-based classes.
Debugging stability
A new debugging utility provideStabilityDebugging() helps developers troubleshoot applications
that fail to stabilize during hydration.
If your application doesn't reach a stable state within 9 seconds, this utility logs diagnostic information to the console, including pending tasks and their stack traces. This is particularly useful when debugging hydration timeouts in SSR applications.
import { provideStabilityDebugging } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [provideStabilityDebugging()],
});
Note that this utility is provided by default in dev mode when using provideClientHydration().
Vitest
The Angular CLI's migration schematic for converting Jasmine tests to Vitest
now supports a browserMode option (a contribution from my fellow ninja JB!).
If you're migrating your tests to Vitest and plan to use Vitest's browser mode (you should, read our blog post about Vitest browser mode), you can now use this option to preserve certain assertions that are natively supported in browser mode:
ng generate refactor-jasmine-vitest --browser-mode
When this option is enabled,
the migration will keep the toHaveClass matcher in its original form
instead of converting it to a different assertion,
since Vitest's browser mode provides its own toHaveClass matcher.
This makes the migration smoother for projects that want to run their tests in a real browser environment, ensuring your tests work as expected without manual adjustments after the migration.
The schematic also now generates a detailed migration report by default,
creating a markdown file that lists all the TODOs and manual tasks
that need to be addressed after the automatic migration.
This report includes precise file paths and line numbers,
making it easier to quickly identify and fix any remaining migration tasks in large codebases.
You can disable this with --no-report if you don't need it.
# Jasmine to Vitest Refactoring Report
Date: 2025-12-17T15:16:43.108Z
## Summary
| | Count |
|:------------------|------:|
| Files Scanned | 159 |
| Files Transformed | 151 |
| Files Skipped | 8 |
| Total TODOs | 3 |
## TODO Overview
| Category | Count |
|:------------|------:|
| addMatchers | 3 |
## Files Requiring Manual Attention
...
AI
The MCP server gained a few new (experimental) tools that enable AI assistants to control Angular projects more effectively:
build: compiles the Angular applicationdevserver.start: launches the development serverdevserver.stop: terminates the running development serverdevserver.wait_for_build: waits until the dev server completes its build processtest: runs unit tests viang teste2e: runs end-to-end tests viang e2e
These tools allow AI assistants like Claude to programmatically build your application, manage the development server, and run your tests, making it easier to verify that everything compiles correctly and all tests pass.
You can now enable all experimental tools at once using the all group in your MCP configuration,
making it easier to get started with the full suite of AI assistance features:
ng mcp --experimental-tool=all.
The already existing ai_tutor tool continues to evolve.
A new lesson has been added to the interactive learning experience: Signal Forms!
If you're curious about Signal Forms, the AI tutor can be a nice resource, but you should definitely check out our 2 part series on Signal Forms if you haven't already 😉
The Angular team continues to improve the infrastructure for AI assistance.
Code examples are now embedded directly in Angular packages (starting with @angular/forms)
using a SQLite database,
making them easily accessible to the MCP server and other tooling.
This builds upon the code examples feature introduced in earlier versions,
improving the AI's ability to provide contextual code snippets and guidance,
and we'll probably see more code examples added in future releases in the various packages.
Finally, setting up the MCP server is now easier than ever.
New Angular workspaces now include a .vscode/mcp.json.template file
with a pre-configured setup for the Angular CLI MCP server.
This eliminates the need for manual configuration
and makes it straightforward to start using AI assistance in your Angular projects.
You can learn more about this on the Angular MCP documentation page.
Summary
That's all for this release!
Signal Forms continue to improve with CSS class configuration, custom control directives support,
the focusBoundControl() method, and naming improvements.
The router gained an experimental integration with the Navigation API.
The CLI team focused on AI features and the Vitest stabilization.
Stay tuned!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
← Older post
What's new in Angular 21.0?
Our books on sale


Next training sessions
- From Jan 19 to Jan 22, 2026Angular: Ninja to Hero(remote)
- From Feb 9 to Feb 12, 2026Vue: Zero to Ninja(remote)
- From Mar 2 to Mar 5, 2026Angular: Zero to Ninja(remote)
- From Mar 16 to Mar 19, 2026Angular: Ninja to Hero(remote)
