Skip to content
Angular ng templates 4 min read

Control Flow: @switch

When a template needs to pick exactly one of several mutually exclusive views based on a single value, chaining @if/@else if quickly becomes noisy. Angular’s @switch block, introduced as part of the built-in control flow in Angular 17, gives you a clean, declarative multi-branch construct that reads like a TypeScript switch statement. It works in any standalone component template with zero imports, and it integrates naturally with signals.

The basic shape

A @switch block evaluates a single expression once, then renders the first @case whose value matches. An optional @default block renders when no case matches. Only one branch is ever instantiated in the DOM.

@switch (status()) {
  @case ('loading') {
    <app-spinner />
  }
  @case ('error') {
    <p class="error">Something went wrong. Please retry.</p>
  }
  @case ('ready') {
    <app-dashboard [data]="data()" />
  }
  @default {
    <p>Unknown state.</p>
  }
}

The block is part of the template language itself, so there is nothing to import. This replaces the older structural-directive approach ([ngSwitch], *ngSwitchCase, *ngSwitchDefault) that required CommonModule.

Matching semantics

Matching uses strict equality (===) between the switch expression and each @case value. There is no fall-through: matching a case does not continue into the next one, so you never need break. If multiple cases share a value, only the first matching one renders.

import { Component, signal } from '@angular/core';

type Plan = 'free' | 'pro' | 'enterprise';

@Component({
  selector: 'app-plan-badge',
  standalone: true,
  template: `
    <button (click)="cycle()">Cycle plan</button>
    @switch (plan()) {
      @case ('free') {
        <span class="badge">Free tier · limited features</span>
      }
      @case ('pro') {
        <span class="badge badge--pro">Pro · {{ seats() }} seats</span>
      }
      @case ('enterprise') {
        <span class="badge badge--ent">Enterprise · unlimited</span>
      }
      @default {
        <span class="badge">Select a plan</span>
      }
    }
  `,
})
export class PlanBadgeComponent {
  protected readonly plan = signal<Plan>('free');
  protected readonly seats = signal(5);

  protected cycle(): void {
    const order: Plan[] = ['free', 'pro', 'enterprise'];
    const next = order[(order.indexOf(this.plan()) + 1) % order.length];
    this.plan.set(next);
  }
}

Because the switch expression is a signal call, the matched branch updates automatically whenever the signal changes. Clicking the button cycles through the plans and swaps the rendered badge.

Output:

[Free tier · limited features]   (initial)
[Pro · 5 seats]                  (after one click)
[Enterprise · unlimited]         (after two clicks)

Matching against constants and enums

@case values can be any expression, but they are most readable as literals or component members. Enums work well because they keep the magic strings out of the template.

import { Component, signal } from '@angular/core';

enum OrderStage {
  Pending = 'PENDING',
  Shipped = 'SHIPPED',
  Delivered = 'DELIVERED',
}

@Component({
  selector: 'app-order-status',
  standalone: true,
  template: `
    @switch (stage()) {
      @case (Stage.Pending) {
        <p>Your order is being prepared.</p>
      }
      @case (Stage.Shipped) {
        <p>On its way! Track your parcel.</p>
      }
      @case (Stage.Delivered) {
        <p>Delivered. Enjoy!</p>
      }
    }
  `,
})
export class OrderStatusComponent {
  protected readonly stage = signal<OrderStage>(OrderStage.Pending);
  // Expose the enum to the template
  protected readonly Stage = OrderStage;
}

Templates cannot reference an enum directly — they only see component class members. Assign the enum to a field (here Stage) so @case (Stage.Pending) resolves.

Note there is no @default above. When no case matches, nothing is rendered, which is perfectly valid.

@switch vs nested @if

For two outcomes, @if/@else is simpler. Reach for @switch when you branch on one value across three or more discrete states. The table below summarizes the trade-offs.

Concern@switchChained @if / @else if
Best forOne value, many discrete statesBoolean / heterogeneous conditions
ComparisonStrict === against each caseAny boolean expression
Default branch@defaultTrailing @else
Readability at 3+ branchesHighDegrades quickly
Fall-throughNone (no break needed)N/A

Comparison with the legacy syntax

The new block-based control flow is the recommended approach and is what ng generate scaffolds. The legacy directive form still works but requires importing CommonModule.

FeatureBuilt-in control flowLegacy directives
Switch@switch (expr) { ... }[ngSwitch]="expr"
Case@case (value) { ... }*ngSwitchCase="value"
Default@default { ... }*ngSwitchDefault
Import neededNoneCommonModule

You can migrate an entire project automatically:

ng generate @angular/core:control-flow

Best practices

  • Prefer @switch over a long @if/@else if chain once you branch on a single value across three or more states.
  • Always include a @default branch when the switch value could fall outside the known cases — it makes “unhandled” states explicit instead of silently rendering nothing.
  • Drive the switch expression from a signal or computed so the matched branch reacts automatically to state changes.
  • Expose enums and constants as component fields rather than hard-coding string literals in @case, which keeps values type-checked and refactor-safe.
  • Keep each @case body small; if a branch grows large, extract it into a dedicated child component.
  • Run ng generate @angular/core:control-flow to migrate legacy [ngSwitch] usage and drop the CommonModule dependency.
Last updated June 14, 2026
Was this helpful?