Skip to content
Angular ng templates 4 min read

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 no CommonModule are 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 @if condition. They run on every change detection cycle. Use a signal or a computed() 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 as alias to unwrap nullable values and get proper type narrowing inside the block.
  • Reach for @if when you genuinely want to add/remove DOM; use [hidden] or CSS when you only want to toggle visibility and keep state.
  • Always provide an @else branch for user-facing async states (loading, empty, error) instead of leaving a blank gap.
  • Run the ng generate @angular/core:control-flow schematic 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.
Last updated June 14, 2026
Was this helpful?