Jul 15, 2026

Angular Control Flow: @if, @for and @switch Explained

angularcontrol-flowfrontendtypescript

@if, @for, and @switch replace ngIf, ngFor, and ngSwitch since Angular 17. Learn the new control flow syntax and migrate automatically with ng generate.

TL;DR: @if, @for, and @switch replace *ngIf, *ngFor, and *ngSwitch. No imports needed, @else works without ng-template, and @for requires a track expression. Migrate an existing project automatically with ng generate @angular/core:control-flow.

Angular’s template control flow is now declarative. Introduced in Angular 17, stable since Angular 18, and the default in new projects since Angular 19, the block syntax replaces the structural directives ngIf, ngFor, and ngSwitch — which are soft-deprecated today.

Here’s the old way versus the new way.

@if: conditions without ng-template

The classic syntax needed a template reference for the else case:

<div *ngIf="isLoggedIn; else notAuthenticated"></div> <ng-template #notAuthenticated> </ng-template>

And if you wanted to handle the logged-in branch separately too:

<div *ngIf="isLoggedIn; then authenticated else notAuthenticated"></div> <ng-template #authenticated> </ng-template> <ng-template #notAuthenticated> </ng-template>

The same logic with the new Control Flow:

@if (isLoggedIn) { <p>Welcome back!</p> } @else { <p>Please log in.</p> }

No references, no ng-template, and the branches read top to bottom like regular code.

@for: loops with mandatory track

The old syntax:

<div *ngFor="let country of countries"> <option value="{{ country.id }}">{{ country.name }}</option> </div>

The new syntax:

@for (country of countries; track country.id) { <option value="{{ country.id }}">{{ country.name }}</option> }

The track expression is required — that’s the biggest behavioral difference. With *ngFor, trackBy was optional and most codebases skipped it, silently paying the DOM-reconciliation cost. Compare what tracking used to require:

<div *ngFor="let country of countries; trackBy: trackByFn"> <option value="{{ country.id }}">{{ country.name }}</option> </div>
trackByFn(_: number, country: Country) { return country.id; }

A whole component method, replaced by two words in the template. @for also gives you @empty for free:

@for (country of countries; track country.id) { <option value="{{ country.id }}">{{ country.name }}</option> } @empty { <p>No countries found.</p> }

@switch: cases without directives

@switch (language) { @case ('python') { <span>The selected programming language is: Python</span> } @case ('ruby') { <span>The selected programming language is: Ruby</span> } @case ('java') { <span>The selected programming language is: Java</span> } @default { <span>No programming language selected</span> } }

That’s a significant change compared to:

<section [ngSwitch]="language"> <span *ngSwitchCase="'python'">The selected programming language is: Python</span> <span *ngSwitchCase="'ruby'">The selected programming language is: Ruby</span> <span *ngSwitchCase="'java'">The selected programming language is: Java</span> <span *ngSwitchDefault>No programming language selected</span> </section>

The template shrinks considerably and becomes more readable — and therefore more maintainable.

Deferrable Views

The same block syntax powers @defer, which lazy loads template content based on triggers:

@for (book of books; track book.id) { @defer (on viewport) { <book-card [book]="book" /> } }

@defer deserves its own deep dive — triggers, placeholders, prefetching, and bundle-splitting behavior are covered in Angular @defer: Complete Guide to Deferrable Views.

Where did this syntax come from?

The design came out of public RFCs for Control Flow and Deferrable Views. Early proposals used HTML-like tags in the style of {#if}, {:else}, and {/if}, which could get confusing — after a community vote, the consensus landed on the @-syntax we have today.

Migrating an existing project

One command:

ng generate @angular/core:control-flow

It converts *ngIf, *ngFor, and *ngSwitch across your templates automatically. Both syntaxes coexist, so you can also migrate incrementally — but with the old directives soft-deprecated since Angular 19, the direction is clear.


Control Flow is one half of declarative Angular — the other half is reactive state. See Angular Signals Explained: signal(), computed() & effect().