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:
| Binding | Syntax | Direction | Example |
|---|---|---|---|
| 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, whereasvalue="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/@switchcontrol flow over the legacy*ngIf/*ngFor/*ngSwitchdirectives in new code. - Always provide a meaningful
trackexpression 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 asdisabledorhidden. - Read signals by calling them (
name()) inside templates so change detection tracks the dependency correctly. - Favour standalone components and the
importsarray overNgModuledeclarations.