Components own a patch of screen; directives attach behavior to elements that already exist. Angular's template control flow (@if, @for, @switch), the attribute directives (ngClass, ngStyle, ngModel), and your own custom directives cover the "make this element behave differently" space. This overview maps the landscape on Angular 20 with a working demo — including a custom hover-highlight directive — all verified with ng build.

Control flow: the modern structural directives
What *ngIf/*ngFor did, built-in block syntax now does better:
<ul>
@for (p of visibleProducts(); track p.sku) {
<li>{{ p.name }} — {{ p.stock }} left</li>
} @empty {
<li>No products match.</li>
}
</ul>
@switch (status()) {
@case ('loading') { <p>Loading…</p> }
@case ('ready') { <p>{{ visibleProducts().length }} products shown</p> }
@default { <p>Something went wrong.</p> }
}
Why the new syntax won: no CommonModule import, sharper type narrowing inside @if, an @empty block *ngFor never had, and mandatory track. That last one is the quiet performance fix — without a track expression, replacing an array re-creates every DOM node; with track p.sku, Angular moves and updates only what changed. The old *ngFor made tracking optional and a decade of apps paid for it. (@if also replaces the *ngIf="x as y" idiom with @if (expr; as y).)
Migration is mechanical — ng generate @angular/core:control-flow converts an existing codebase.
Attribute directives: styling and forms
Attribute directives modify the element they sit on:
<li [ngClass]="{ 'sold-out': p.stock === 0 }"
[ngStyle]="{ fontWeight: p.stock === 0 ? '400' : '600' }">
ngClasstoggles classes from an object/array — for one class, plain[class.sold-out]="p.stock === 0"is lighter and needs no import.ngStylesets inline styles from an object; same story — single styles read better as[style.fontWeight]="...".ngModel(fromFormsModule) two-way binds form controls:
<label><input type="checkbox" [(ngModel)]="showSoldOut" /> show sold-out</label>
The banana-in-a-box [(ngModel)] is sugar for a property binding plus event binding — fine for simple forms; reactive forms take over when validation gets real.
Writing a custom directive
The demo's appHighlight colors any element on hover:
import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';
@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
private readonly el = inject(ElementRef<HTMLElement>);
readonly appHighlight = input<string>('#fff3bf'); // configurable color
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.appHighlight();
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
Usage — import it like a component, apply it like an attribute:
<li appHighlight="#d3f9d8" [ngClass]="...">{{ p.name }}</li>
The anatomy: an attribute selector [appHighlight]; ElementRef for the host element via inject(); @HostListener for host events (there's a host: {} metadata alternative); and an input() named after the selector, which is what lets appHighlight="#d3f9d8" pass the color and apply the directive in one attribute. Directives compose — the same <li> carries appHighlight, ngClass, and ngStyle, each adding one behavior; that's the design intent, small orthogonal behaviors rather than mega-components.
Real-world custom directive material: autofocus on appear, click-outside detection, permission-based hiding (*appHasRole="'admin'"), input masks, tooltip attachment. The rule of thumb: behavior without markup = directive; behavior with markup = component.
Structural directives, the custom kind
You can still write custom structural directives (the * kind that add/remove template chunks) with TemplateRef and ViewContainerRef — permission gates being the classic. With @if covering conditionals and @defer covering lazy rendering, reach for a custom structural directive only when you truly need template-manipulation semantics the blocks don't express; most cases today are an @if around a computed().
Picking the right tool
| Need | Reach for |
|---|---|
| Conditional/repeated markup | @if / @for (with track) |
| Toggle one class or style | [class.x] / [style.y] |
| Multiple classes/styles from state | ngClass / ngStyle |
| Form field binding | [(ngModel)], then reactive forms |
| Reusable element behavior | custom attribute directive |
| Reusable UI with its own markup | a component |
The demo component with all of the above — control-flow blocks, the checkbox filter, and the custom highlight directive — builds and runs in the companion repository (npm install && ng serve), alongside the lifecycle and nested-component samples from this series.
Comments (0)
No comments yet — be the first to share your thoughts.