Directives
By Syncfusion®, Inc. · Last updated
What directives are, the structural directives *ngIf, *ngFor and *ngSwitch, the modern @if, @for and @switch blocks, the attribute directives ngClass and ngStyle, best practices, and a first custom directive.
What Are Directives?
A directive is a class that adds behavior to an element in a template. Angular applies the directive wherever its selector appears in the HTML.
There are three kinds of directives:
- Components. Directives with their own template. Every component you have written so far is a directive.
- Structural directives. Add, remove or repeat elements in the page, such as
*ngIfand*ngFor. - Attribute directives. Change the appearance or behavior of an existing element, such as
ngClassandngStyle.
Real-World Example
In an admin dashboard, a structural directive renders the Delete button only for users with the admin role, and another repeats one table row for each record returned by the API.
An attribute directive highlights overdue invoices in red, and a custom appAutofocus directive puts the cursor in the search box when the page opens.
Directiveadds behaviorComponenthas its own template<app-card>Structuraladds or removes elements@if, @for, *ngIfAttributechanges look or behaviorngClass, appHighlight
Every component is a directive. Structural and attribute directives add behavior to existing elements.
Structural Directives
A structural directive changes the structure of the page. It decides whether an element exists in the DOM and how many copies of it exist.
Structural directives start with an asterisk (*). The asterisk is a short form: Angular wraps the element in an <ng-template> and lets the directive decide when to render it.
The classic structural directives come from @angular/common. Import the ones you use into the component's imports list.
*ngIf
*ngIf adds the element to the page when its condition is true and removes it when the condition is false. It can show an alternative template with else.
*ngIf renders the right message for the login state
import { Component } from '@angular/core';
import { NgIf } from '@angular/common';
@Component({
selector: 'app-root',
imports: [NgIf],
template: `
<p *ngIf="isLoggedIn; else guest">Welcome back, {{ userName }}.</p>
<ng-template #guest><p>Please sign in to continue.</p></ng-template>
<button (click)="isLoggedIn = !isLoggedIn">Toggle login</button>
`,
})
export class App {
isLoggedIn = true;
userName = 'Asha';
}
*ngIf removes the element from the DOM. It does not hide it with CSS. A removed component is destroyed, and its state is lost.
*ngFor
*ngFor renders the element once for each item in a list. It also provides helper values such as index, first and last.
*ngFor renders one row for each order
import { Component } from '@angular/core';
import { NgFor } from '@angular/common';
interface Order {
id: number;
total: number;
}
@Component({
selector: 'app-root',
imports: [NgFor],
template: `
<ul>
<li *ngFor="let order of orders; let i = index; trackBy: trackById">
{{ i + 1 }}. Order #{{ order.id }} · ₹{{ order.total }}
</li>
</ul>
`,
})
export class App {
orders: Order[] = [
{ id: 501, total: 1299 },
{ id: 502, total: 349 },
{ id: 503, total: 2450 },
];
trackById(index: number, order: Order) {
return order.id;
}
}
trackBy tells Angular how to identify each item. When the list changes, Angular reuses the existing rows for items with the same id instead of rebuilding the whole list.
*ngSwitch
*ngSwitch picks one template out of several, based on a value. It uses three directives together:
[ngSwitch]on the container holds the value to compare.*ngSwitchCasemarks each possible match.*ngSwitchDefaultmarks the template used when nothing matches.
*ngSwitch shows a label for each payment status
import { Component } from '@angular/core';
import { NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
@Component({
selector: 'app-root',
imports: [NgSwitch, NgSwitchCase, NgSwitchDefault],
template: `
<div [ngSwitch]="status">
<p *ngSwitchCase="'paid'">✅ Payment received</p>
<p *ngSwitchCase="'pending'">⏳ Waiting for payment</p>
<p *ngSwitchCase="'failed'">❌ Payment failed</p>
<p *ngSwitchDefault>Unknown status</p>
</div>
`,
})
export class App {
status = 'pending';
}
Change status to 'paid' or 'failed' and run it again.
Modern Control Flow
Since Angular 17, templates have built-in control flow blocks: @if, @for and @switch. They do the same jobs as *ngIf, *ngFor and *ngSwitch, with a syntax closer to JavaScript.
Control flow blocks are part of the template language. You do not import anything to use them. New Angular code uses them by default.
@if
@if renders its block when the condition is true. You can add @else if and @else blocks.
@if chooses the stock message
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h4>Wireless Mouse</h4>
@if (stock === 0) {
<p>Out of stock</p>
} @else if (stock < 5) {
<p>Only {{ stock }} left. Order soon.</p>
} @else {
<p>In stock</p>
}
`,
})
export class App {
stock = 3;
}
@for
@for repeats its block for each item in a list. The track expression is required. It tells Angular how to identify each item, like trackBy does for *ngFor.
An @empty block renders when the list has no items.
@for lists tasks and handles an empty list
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h4>Sprint tasks</h4>
<ul>
@for (task of tasks; track task.id; let i = $index) {
<li>{{ i + 1 }}. {{ task.title }}</li>
} @empty {
<li>No tasks in this sprint.</li>
}
</ul>
<button (click)="tasks = []">Clear tasks</button>
`,
})
export class App {
tasks = [
{ id: 1, title: 'Design the login page' },
{ id: 2, title: 'Connect the orders API' },
{ id: 3, title: 'Write unit tests' },
];
}
Click Clear tasks. The @empty block replaces the list.
@switch
@switch compares one value against several @case blocks and renders the first match. @default renders when no case matches.
@switch shows the permissions for a role
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Role: {{ role }}</p>
@switch (role) {
@case ('admin') { <p>Can create, edit and delete users.</p> }
@case ('editor') { <p>Can edit content.</p> }
@default { <p>Read-only access.</p> }
}
`,
})
export class App {
role = 'editor';
}
| Task | Classic directive | Control flow block |
|---|---|---|
| Show or hide | *ngIf="cond; else other" | @if (cond) { } @else { } |
| Repeat | *ngFor="let x of list; trackBy: fn" | @for (x of list; track x.id) { } |
| Choose one | [ngSwitch] + *ngSwitchCase | @switch (value) { @case (...) { } } |
| Import needed | Yes, from @angular/common | No |
| Empty-list template | Write your own *ngIf | Built in: @empty |
| Tracking items | Optional trackBy | Required track |
Use @if, @for and @switch in new code. You still need to read *ngIf, *ngFor and *ngSwitch, because most existing Angular projects use them.
Attribute Directives
An attribute directive changes how an existing element looks or behaves. It does not add or remove elements.
ngClass
ngClass adds and removes several CSS classes at once. It accepts a string, an array, or an object whose keys are class names and whose values are conditions.
ngClass applies classes from conditions
import { Component } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'app-root',
imports: [NgClass],
template: `
@for (invoice of invoices; track invoice.id) {
<p [ngClass]="{ overdue: invoice.daysLate > 0, paid: invoice.paid }">
Invoice {{ invoice.id }}: {{ invoice.paid ? 'Paid' : invoice.daysLate + ' days late' }}
</p>
}
`,
styles: `
p { padding: 4px 8px; border-radius: 6px; }
.overdue { background: #fee2e2; color: #991b1b; font-weight: bold; }
.paid { background: #dcfce7; color: #166534; }
`,
})
export class App {
invoices = [
{ id: 'INV-101', paid: true, daysLate: 0 },
{ id: 'INV-102', paid: false, daysLate: 12 },
];
}
ngStyle
ngStyle sets several inline styles at once from an object. Keys are CSS property names, and you can add units to the key, such as 'width.px'.
ngStyle sets styles from calculated values
import { Component } from '@angular/core';
import { NgStyle } from '@angular/common';
@Component({
selector: 'app-root',
template: `
<p>Disk usage: {{ used }}%</p>
<div class="track">
<div [ngStyle]="{ 'width.%': used, 'background-color': used > 80 ? '#dc2626' : '#2563eb' }" class="bar"></div>
</div>
`,
imports: [NgStyle],
styles: `
.track { width: 240px; height: 10px; background: #e5e7eb; border-radius: 5px; overflow: hidden; }
.bar { height: 100%; }
`,
})
export class App {
used = 86;
}
| Point | ngClass | [class.x] binding | ngStyle | [style.x] binding |
|---|---|---|---|---|
| Changes | Several classes | One class | Several styles | One style |
| Import needed | Yes (NgClass) | No | Yes (NgStyle) | No |
| Best for | Many conditional classes | One condition | Several calculated values | One calculated value |
Directive Best Practices
Follow these rules to keep templates fast and easy to read:
- Prefer control flow blocks. Use
@if,@forand@switchin new code. They need no imports and give better error messages. - Always track list items by a stable id. Use
track item.id, nottrack $index, when items can be added, removed or reordered. - Keep logic out of the template. Move long conditions into a class property or method with a clear name, such as
canEdit. - Prefer class binding to style binding. Keep colors and sizes in the stylesheet, and switch classes from the template.
- Do not hide with
@ifwhat you only want to hide visually.@ifdestroys the content. For a quick show/hide that keeps state, bind[hidden]or a CSS class. - Write a custom directive for repeated behavior. If several templates need the same DOM behavior, put it in one directive instead of copying code.
Never put two structural directives on the same element, such as *ngIf and *ngFor together. Angular reports an error. Wrap the element in an <ng-container> and put one directive on each, or use @if inside @for.
Introduction to Custom Directives
A custom directive is your own attribute directive. It packages DOM behavior so you can add it to any element with one attribute.
You create one with the @Directive decorator. The selector is usually an attribute in square brackets, such as '[appHighlight]'. In a project, the CLI creates it for you:
ng generate directive highlight
Inside the directive:
inject(ElementRef)gives access to the element the directive is attached to.@HostListener('event')runs a method when that element fires an event.@Input()lets the template pass settings to the directive.
A custom directive highlights rows on hover
import { Component, Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class Highlight {
private el = inject(ElementRef);
@Input() appHighlight = '#fef08a';
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
@Component({
selector: 'app-root',
imports: [Highlight],
template: `
<p appHighlight>Hover me: default yellow</p>
<p [appHighlight]="'#bfdbfe'">Hover me: custom blue</p>
`,
})
export class App {}
Move the mouse over each paragraph. The same directive applies a different color to each, because the color is passed through the input.
The square brackets in selector: '[appHighlight]' mean "match any element that has the attribute appHighlight". Without brackets, 'appHighlight' would match an element named <appHighlight>, as a component selector does.
Custom directives are covered in depth in a later lesson.
Common Mistakes
| Mistake | What you see | Fix |
|---|---|---|
*ngIf used without importing NgIf | The element always renders, and the console shows NG0303 | Add NgIf to imports, or use @if |
@for without track | A template parse error: @for loop must have a "track" expression | Add track item.id |
*ngIf and *ngFor on one element | An error about multiple template bindings | Use <ng-container> for one of them |
track $index on a list that changes order | Rows show the wrong state after sorting | Track a stable id, such as track item.id |
Missing quotes in *ngSwitchCase="paid" | The case never matches | Compare to a string: *ngSwitchCase="'paid'" |
Forgetting the brackets in [ngClass] | The text { overdue: ... } becomes a class name | Bind it: [ngClass]="{ ... }" |
Check Yourself
Exercises
What to Remember
- Directives add behavior to elements. Structural directives change which elements exist, and attribute directives change how elements look or behave.
- Use
@if,@for(withtrack) and@switchin new code, and recognise*ngIf,*ngForand*ngSwitchin existing code. ngClassandngStyleset several classes or styles at once. A custom directive packages reusable DOM behavior behind one attribute.
Next: Pipes, how to format dates, currency and text directly in the template.