Skip to content
Angular ng templates 4 min read

Template Syntax Overview

Angular templates are HTML enriched with a small, declarative syntax that connects your markup to component state. Instead of imperatively poking at the DOM, you describe what the UI should look like for a given state, and Angular keeps the rendered output in sync as that state changes. This page is a high-level tour of the building blocks — interpolation, the binding family, directives, and the modern control flow — so you can recognise each piece before diving into the dedicated pages.

A template is HTML plus four superpowers

A component template is valid HTML that Angular augments with four categories of syntax: interpolation, bindings, directives, and control flow. Everything you write in a template ultimately resolves to one of these. In a standalone component (the default since Angular 17), the template lives inline via template or in an external file via templateUrl.

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

@Component({
  selector: 'app-greeting',
  template: `
    <h1>Hello, {{ name() }}!</h1>
    <button (click)="cheer()">Cheer</button>
    <p [class.loud]="excited()">{{ message() }}</p>
  `,
})
export class GreetingComponent {
  name = signal('Ada');
  excited = signal(false);
  message = signal('Welcome to Angular.');

  cheer() {
    this.excited.set(true);
    this.message.set('WELCOME TO ANGULAR!');
  }
}

That tiny template already uses three of the four superpowers: interpolation, an event binding, and a class binding.

Interpolation: rendering values

Interpolation uses the double-curly {{ }} syntax to embed a computed string into the DOM. The expression inside is evaluated against the component instance and re-evaluated whenever its dependencies change. Because signals are functions, you call them inside the braces.

<p>Total: {{ price() * quantity() }}</p>
<p>{{ user().firstName + ' ' + user().lastName }}</p>

Template expressions are deliberately restricted — no assignments, no new, no chained side effects — which keeps templates predictable and side-effect free.

Bindings: the data flow directions

Bindings connect a template to component members. They differ by direction of data flow:

BindingSyntaxDirectionExample
Property[prop]="expr"Component → DOM<img [src]="avatarUrl()">
Attribute[attr.x]="expr"Component → DOM<td [attr.colspan]="span()">
Class[class.x]="bool"Component → DOM<div [class.active]="isOpen()">
Style[style.x]="val"Component → DOM<div [style.width.px]="w()">
Event(event)="handler()"DOM → Component<button (click)="save()">
Two-way[(ngModel)]="expr"Both<input [(ngModel)]="email">

Property binding pushes a value into an element or component input. Event binding listens for DOM events or component outputs and runs a statement when they fire. Two-way binding is sugar that combines a property binding with an event binding using the “banana in a box” [()] syntax.

@Component({
  selector: 'app-toggle',
  template: `
    <button [disabled]="busy()" (click)="onToggle()">
      {{ open() ? 'Close' : 'Open' }}
    </button>
  `,
})
export class ToggleComponent {
  open = signal(false);
  busy = signal(false);

  onToggle() {
    this.open.update((v) => !v);
  }
}

Tip: Square brackets bind to a property, not an HTML attribute. [value]="x" sets the live DOM property, whereas value="x" sets the static initial attribute. They are not interchangeable.

Directives: reusable template behaviour

Directives attach behaviour to elements. Attribute directives change appearance or behaviour (for example ngClass, ngStyle, or your own), while components are themselves directives with a template. You apply them through their selector and configure them with bindings.

<div [ngClass]="{ card: true, 'is-selected': selected() }">…</div>

In modern Angular you wire directives into a standalone component through its imports array rather than an NgModule.

Control flow: the new built-in blocks

Angular 17 introduced a built-in, block-based control flow that replaces the old *ngIf, *ngFor, and *ngSwitch structural directives. It is faster, type-checked, and needs no imports.

@if (user(); as u) {
  <p>Signed in as {{ u.name }}</p>
} @else {
  <a routerLink="/login">Sign in</a>
}

<ul>
  @for (item of items(); track item.id) {
    <li>{{ item.label }}</li>
  } @empty {
    <li>No items yet.</li>
  }
</ul>

@switch (status()) {
  @case ('loading') { <app-spinner /> }
  @case ('error')   { <app-error /> }
  @default          { <app-content /> }
}

The track expression in @for is mandatory — it tells Angular how to identify each row so it can reuse DOM nodes efficiently instead of re-rendering the whole list.

For lazy-loading parts of a template, the @defer block streams content in based on triggers like viewport visibility:

@defer (on viewport) {
  <app-heavy-chart [data]="metrics()" />
} @placeholder {
  <div class="skeleton"></div>
}

Output: what the rendered list looks like with three items.

• Write template
• Add bindings
• Ship feature

Best practices

  • Prefer the built-in @if/@for/@switch control flow over the legacy *ngIf/*ngFor/*ngSwitch directives in new code.
  • Always provide a meaningful track expression in @for (a stable id) to avoid unnecessary DOM churn.
  • Keep template expressions simple and pure; move real logic into computed() signals or methods on the component.
  • Use property binding [prop] instead of string interpolation when the target is a non-string property such as disabled or hidden.
  • Read signals by calling them (name()) inside templates so change detection tracks the dependency correctly.
  • Favour standalone components and the imports array over NgModule declarations.
Last updated June 14, 2026
Was this helpful?