The New Control Flow Syntax (@if, @for, @switch)
Starting with Angular 17, the framework introduced a new built-in control-flow syntax — @if, @for, and @switch — as a direct, more ergonomic replacement for the structural directives *ngIf, *ngFor, and *ngSwitch. This syntax is part of the template language itself rather than a directive imported from CommonModule, which means no imports are required to use it in any component, standalone or not. Under the hood, the new blocks compile to more efficient instructions than the directive-based versions, and Angular's benchmarks show meaningful improvements in rendering and change-detection performance, particularly for large lists. As of Angular 17, this syntax is stable, and Angular 20's schematics can automatically migrate an existing codebase's *ngIf/*ngFor usage to the block syntax.
Cricket analogy: The new @if/@for/@switch syntax is like a ground switching from hand signals relayed by multiple umpires to a single built-in electronic signal system understood instantly by everyone on the field, no separate translator needed, and faster to read on big screens than the old flag system.
@if and @else
@if reads much like a native TypeScript or JavaScript if statement: @if (condition) { ... } @else if (other) { ... } @else { ... }. Unlike *ngIf, there is no need for a separate <ng-template> and a #ref for the else branch — @else and @else if are simply chained blocks. You can also alias the evaluated expression using the as keyword, similar to *ngIf's as clause, which is particularly handy for narrowing a nullable value or the resolved value of a signal or async pipe once inside the block.
Cricket analogy: @if with @else if and @else chained directly is like an umpire's decision tree read straight off a single rulebook page (out, not out, review) without flipping to a separate appendix, and the as alias is like the third umpire narrowing a blurry replay down to one confirmed frame to work with.
@Component({
selector: 'app-status-badge',
standalone: true,
template: `
@if (order().status === 'pending') {
<span class="badge badge-warning">Pending</span>
} @else if (order().status === 'shipped') {
<span class="badge badge-info">Shipped</span>
} @else {
<span class="badge badge-success">Delivered</span>
}
`
})
export class StatusBadgeComponent {
order = input.required<Order>();
}@for and the Mandatory track Expression
@for (item of items; track item.id) { ... } replaces *ngFor, but with one deliberate change: the track expression is mandatory, not optional. Angular's team made trackBy opt-out-only via track $index because omitting stable tracking was such a common source of performance bugs with *ngFor. @for also exposes a richer implicit context object via the $ variable — $index, $first, $last, $even, $odd, and $count — accessed inline in the template without needing let aliases. @for additionally supports an @empty block, rendered when the iterable has zero items, eliminating the old pattern of pairing *ngFor with a sibling *ngIf="items.length === 0".
Cricket analogy: @for with mandatory track item.id is like an umpire being required to track each fielder by their fixed shirt number rather than just their position on the field, preventing confusion when players swap positions mid-over, with $index, $first, and $last giving instant context like knowing you're facing the very first ball of a new over, and @empty rendering a 'no play today' notice when rain cancels the match.
@Component({
selector: 'app-cart',
standalone: true,
template: `
<ul>
@for (line of cartLines(); track line.sku) {
<li>{{ $index + 1 }}. {{ line.name }} — {{ line.qty }}×</li>
} @empty {
<li>Your cart is empty.</li>
}
</ul>
`
})
export class CartComponent {
cartLines = signal<CartLine[]>([]);
}@switch, @case, and @default
@switch (expr) { @case (value) { ... } @default { ... } } mirrors *ngSwitch/*ngSwitchCase/*ngSwitchDefault but, like @if, requires no directive imports and reads closer to native switch statements. Angular uses strict equality (===) to match @case values against the switch expression, exactly as the old ngSwitch directive did, so type mismatches (e.g. comparing a string to a number) silently fall through to @default.
Cricket analogy: @switch with strict equality is like a match referee comparing a dismissal type against a fixed rulebook list using an exact match, so a technicality like comparing 'LBW' as text against a numeric code silently falls through to a @default 'no decision' ruling rather than throwing an error mid-match.
Because @if/@for/@switch are parsed directly by the Angular template compiler rather than resolved as directives, they carry effectively zero import overhead and cannot be 'shadowed' or misconfigured the way a missing NgFor import breaks *ngFor. This is one reason Angular's official docs and CLI schematics (ng generate, and the ng update migration for v17+) now default to the new syntax for all new templates.
Forgetting the track expression in @for is a compile-time error, not a silent performance footgun — this is intentional. If you don't have a natural unique key, you can fall back to track $index, but doing so reintroduces the same reordering/DOM-churn issues that untracked *ngFor had, so prefer a real identity field whenever one exists.
- @if, @for, and @switch are built into the Angular template compiler — no CommonModule imports needed, in any component.
- @if/@else if/@else reads like native conditional syntax and supports an
asalias, replacing *ngIf and its <ng-template #else>. - @for requires a mandatory
trackexpression, preventing the common 'forgot trackBy' performance bug from *ngFor. - @for provides $index, $first, $last, $even, $odd, $count implicitly, plus an @empty block for empty-iterable UI.
- @switch/@case/@default use strict equality (===), just like the legacy ngSwitch directive family.
- The new syntax is generally faster to render and diff than the directive-based structural directives, and Angular provides an automated migration schematic.
Practice what you learned
1. What must every @for block include that *ngFor did not require?
2. Which imports are required to use @if in a standalone component's template?
3. What does the @empty block in @for do?
4. How does @switch match @case values against the switch expression?
5. Which implicit variable in @for gives the current iteration's zero-based position?
Was this page helpful?
You May Also Like
Structural Directives: *ngIf and *ngFor
Learn how *ngIf and *ngFor reshape the DOM by adding, removing, and repeating elements, plus the microsyntax and ng-template mechanics behind the asterisk.
Introduction to Signals
Understand Angular Signals — the reactive primitive that wraps a value and notifies dependents on change, forming the foundation of Angular's modern change-detection model.
Standalone Components and Bootstrapping
Understand how standalone components eliminate the need for NgModules, and how modern Angular applications bootstrap directly from a root component and providers array.
Template Syntax and Interpolation
How Angular templates blend HTML with expressions, and how interpolation renders component state directly into the DOM as text.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics