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:

Structural directiveNew Control FlowKey difference
*ngIf / *ngIf...else@if / @elseNo ng-template or reference variables needed
*ngFor@fortrack is mandatory, not optional
*ngSwitch / *ngSwitchCase@switch / @caseNo [ngSwitch] directive binding needed

@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.

How to Migrate from *ngIf, *ngFor, and *ngSwitch to Control Flow

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().

Written by

Antonio Cárdenas

Google Developer Expert in Angular · Verified GDE profile · Google Developers profile

My technical writing reaches 100,000+ developers in English and Spanish. I've migrated production Angular apps from v8 to v22 — everything I publish is built from real project work, not documentation re-reads.

Frequently asked questions

FAQs

What is Angular's new Control Flow?

Angular's new Control Flow is a declarative syntax introduced in Angular 17 that replaces the structural directives ngIf, ngFor, and ngSwitch. It uses @ blocks like @if, @for, and @switch directly in the template, with no extra module imports. It is more readable, more efficient, and built directly into the compiler.

How do I migrate from *ngIf to the new @if in Angular?

Replace <div *ngIf='condition'> with @if (condition) { <div> }. For the else case, instead of an ng-template with a reference, use @else { } directly. Angular 17+ ships an automatic migration: ng generate @angular/core:control-flow converts all your templates automatically.

Does the new @for require track?

Yes. Unlike *ngFor where trackBy was optional, the new @for requires a track expression. This improves DOM reconciliation performance. Example: @for (item of items; track item.id) { }. If you don't have a unique identifier you can use track $index, though that is not recommended for dynamic lists.

Which Angular version introduced the new Control Flow?

The new Control Flow shipped as developer preview in Angular 17 (November 2023) and was stabilized as a public API in Angular 18. Since Angular 19 it is the default syntax in new projects generated with ng new.

Can I use ngIf and the new @if in the same Angular project?

Yes. Both syntaxes coexist during migration, so you can migrate component by component. However, the Angular team recommends fully adopting the new Control Flow, since ngIf, ngFor, and ngSwitch are soft-deprecated since Angular 19. Use ng generate @angular/core:control-flow for the automatic migration.

Keep reading

Building something with Angular?

I write about Angular architecture, Signals and the tooling around them — migration guides, real upgrade paths, and the catches most posts skip.

Browse all articlesAbout me

More from the blog