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 *ngIf and *ngFor.
  • Attribute directives. Change the appearance or behavior of an existing element, such as ngClass and ngStyle.

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.
  • *ngSwitchCase marks each possible match.
  • *ngSwitchDefault marks 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';
}
TaskClassic directiveControl 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 neededYes, from @angular/commonNo
Empty-list templateWrite your own *ngIfBuilt in: @empty
Tracking itemsOptional trackByRequired 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;
}
PointngClass[class.x] bindingngStyle[style.x] binding
ChangesSeveral classesOne classSeveral stylesOne style
Import neededYes (NgClass)NoYes (NgStyle)No
Best forMany conditional classesOne conditionSeveral calculated valuesOne calculated value

Directive Best Practices

Follow these rules to keep templates fast and easy to read:

  • Prefer control flow blocks. Use @if, @for and @switch in new code. They need no imports and give better error messages.
  • Always track list items by a stable id. Use track item.id, not track $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 @if what you only want to hide visually. @if destroys 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

MistakeWhat you seeFix
*ngIf used without importing NgIfThe element always renders, and the console shows NG0303Add NgIf to imports, or use @if
@for without trackA template parse error: @for loop must have a "track" expressionAdd track item.id
*ngIf and *ngFor on one elementAn error about multiple template bindingsUse <ng-container> for one of them
track $index on a list that changes orderRows show the wrong state after sortingTrack a stable id, such as track item.id
Missing quotes in *ngSwitchCase="paid"The case never matchesCompare to a string: *ngSwitchCase="'paid'"
Forgetting the brackets in [ngClass]The text { overdue: ... } becomes a class nameBind 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 (with track) and @switch in new code, and recognise *ngIf, *ngFor and *ngSwitch in existing code.
  • ngClass and ngStyle set 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.