Control Flow: @if
Angular 17 introduced a built-in control flow syntax that lives directly in the template language, and @if is its conditional building block. It replaces the long-standing *ngIf structural directive with a cleaner, block-scoped syntax that supports @else if and @else branches out of the box. Because control flow is now part of the compiler rather than a directive, it needs no imports, produces smaller bundles, and renders measurably faster. This page covers how to use @if in modern standalone components, including how it pairs naturally with signals.
Basic syntax
An @if block wraps the template you want to render conditionally inside curly braces. The condition is any expression that evaluates to a truthy or falsy value.
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-greeting',
standalone: true,
template: `
@if (isLoggedIn()) {
<p>Welcome back, friend!</p>
}
`,
})
export class GreetingComponent {
isLoggedIn = signal(true);
}
When isLoggedIn() returns true, the paragraph is created and inserted into the DOM. When it returns false, the block and its contents are removed entirely — not merely hidden with CSS. Calling the signal inside the template means Angular re-evaluates the condition automatically whenever the signal’s value changes.
Adding @else if and @else branches
Unlike *ngIf, which required a clumsy ; else template reference, the new syntax supports chained branches inline. Only the first matching branch renders.
@Component({
selector: 'app-order-status',
standalone: true,
template: `
@if (status() === 'shipped') {
<p class="ok">Your order is on its way.</p>
} @else if (status() === 'processing') {
<p class="pending">We're preparing your order.</p>
} @else {
<p class="muted">No active orders.</p>
}
`,
})
export class OrderStatusComponent {
status = signal<'shipped' | 'processing' | 'none'>('processing');
}
The @else if and @else blocks must immediately follow the closing brace of the preceding branch. The @else branch is optional and acts as the fallback when no condition matches.
Capturing the condition value with as
A common pattern is binding the result of an expression so you can reuse it without re-evaluating. @if supports aliasing the truthy value with as, which is especially handy when unwrapping a possibly-null value or an async result.
@Component({
selector: 'app-profile',
standalone: true,
template: `
@if (user(); as currentUser) {
<h2>{{ currentUser.name }}</h2>
<p>{{ currentUser.email }}</p>
} @else {
<p>Loading profile…</p>
}
`,
})
export class ProfileComponent {
user = signal<{ name: string; email: string } | null>(null);
}
Inside the block, currentUser is narrowed to the non-null type, so TypeScript and the template type checker both treat it as fully defined. This eliminates the optional-chaining noise (user()?.name) you would otherwise need.
Migrating from *ngIf
If you are upgrading an existing codebase, Angular ships an automated schematic that rewrites structural directives to the new control flow blocks.
ng generate @angular/core:control-flow
Output:
✔ Schematic completed.
UPDATE src/app/order-status/order-status.component.ts (412 bytes)
UPDATE src/app/profile/profile.component.ts (308 bytes)
The table below maps the old directive forms onto the new syntax.
*ngIf form | @if equivalent |
|---|---|
*ngIf="cond" | @if (cond) { … } |
*ngIf="cond; else tpl" | @if (cond) { … } @else { … } |
*ngIf="x as y" | @if (x; as y) { … } |
Nested ng-template chains | @if / @else if / @else |
Tip: The new control flow is enabled by default in any project on Angular 17 or later — no
imports: [NgIf]entry and noCommonModuleare required. You can safely delete those imports once migration is complete.
How it differs from hiding with [hidden]
@if adds and removes nodes from the DOM, which destroys child component instances and their state when the condition becomes false. If you need to preserve state or avoid re-initialization cost, prefer the [hidden] attribute or a CSS class, which keeps the element in the DOM and only toggles visibility.
<!-- Destroyed and recreated each toggle -->
@if (showPanel()) {
<expensive-widget />
}
<!-- Stays in the DOM, just visually hidden -->
<expensive-widget [hidden]="!showPanel()" />
Warning: Avoid putting expensive function calls directly in the
@ifcondition. They run on every change detection cycle. Use a signal or acomputed()so evaluation is cached and only recomputes when dependencies change.
Best practices
- Prefer signals or
computed()values in conditions so re-evaluation is reactive and cheap rather than running on every change detection pass. - Use the
asalias to unwrap nullable values and get proper type narrowing inside the block. - Reach for
@ifwhen you genuinely want to add/remove DOM; use[hidden]or CSS when you only want to toggle visibility and keep state. - Always provide an
@elsebranch for user-facing async states (loading, empty, error) instead of leaving a blank gap. - Run the
ng generate @angular/core:control-flowschematic for bulk migrations rather than rewriting templates by hand. - Keep conditions simple and declarative; move complex boolean logic into the component class as a named
computed()signal for readability.