Directives Overview
A directive is a TypeScript class that attaches behavior to elements in the DOM. Whenever Angular renders a template, it scans the markup for selectors that match registered directives and lets each one extend, transform, or replace what the browser would otherwise show. Understanding the three kinds of directives — components, attribute directives, and structural directives — is the foundation for almost everything you build in Angular, because every component you write and every @if/@for block you reach for is a directive under the hood.
What is a directive?
At the language level, a directive is just a class decorated with @Directive (or @Component, which is a specialized directive). The decorator’s selector tells Angular which elements in a template the directive applies to, and the class body holds the logic, inputs, and outputs that run against each matched element. Angular instantiates one directive instance per matching element and wires it into change detection alongside the host element.
import { Directive, ElementRef, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
})
export class HighlightDirective {
private el = inject(ElementRef<HTMLElement>);
constructor() {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}
Apply it by placing the selector on any element:
<p appHighlight>This paragraph now has a yellow background.</p>
In modern Angular (17+), directives and components are standalone by default. You import a directive directly into a component’s
importsarray rather than declaring it in anNgModule.
The three types of directives
Angular groups directives into three categories based on what they affect: the whole element and its template, the element’s appearance or behavior, or the element’s presence in the DOM.
| Type | Decorator | What it does | Examples |
|---|---|---|---|
| Component | @Component | A directive with its own template; renders UI | Every component you write |
| Attribute | @Directive | Changes the appearance or behavior of an existing element | ngClass, ngStyle, ngModel |
| Structural | @Directive | Adds or removes elements from the DOM | *ngIf, *ngFor (legacy); @if/@for (built-in) |
Components
A component is the most common directive. It is defined with @Component, which is @Directive plus a template (and optional styles). Because it owns a template, a component renders a view rather than merely tweaking a host element.
import { Component } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `<h2>Hello, {{ name }}!</h2>`,
})
export class GreetingComponent {
name = 'DevCraftly';
}
Attribute directives
Attribute directives change how an element looks or behaves without changing the DOM structure. They are applied as attributes (hence the name) using a CSS attribute selector like [appHighlight]. Built-in examples include ngClass and ngStyle for conditional styling, and ngModel for two-way binding on form controls.
import { Directive, ElementRef, HostListener, inject, input } from '@angular/core';
@Directive({
selector: '[appHover]',
})
export class HoverDirective {
private el = inject(ElementRef<HTMLElement>);
color = input('lightblue');
@HostListener('mouseenter') onEnter() {
this.el.nativeElement.style.backgroundColor = this.color();
}
@HostListener('mouseleave') onLeave() {
this.el.nativeElement.style.backgroundColor = '';
}
}
<button appHover color="gold">Hover me</button>
Structural directives
Structural directives shape the DOM by adding, removing, or repeating elements. Historically these used the asterisk syntax — *ngIf, *ngFor, *ngSwitch — which Angular desugars into an <ng-template>. Since Angular 17, the built-in control flow blocks @if, @for, and @switch cover the most common cases with better performance and no imports required.
<!-- Built-in control flow (Angular 17+) -->
@if (user(); as u) {
<p>Welcome back, {{ u.name }}.</p>
} @else {
<p>Please sign in.</p>
}
@for (item of items(); track item.id) {
<li>{{ item.label }}</li>
}
You can still author your own structural directives with @Directive and TemplateRef/ViewContainerRef when you need reusable conditional rendering logic.
How Angular matches and runs directives
When a template compiles, Angular records every selector from the directives available to that component (its own imports plus inherited ones). At render time it matches each element against those selectors, instantiates the matching directives, resolves their inputs, and runs lifecycle hooks such as ngOnInit and ngOnChanges. Multiple directives can match the same element — a button can be a component host, carry ngClass, and respond to a custom attribute directive simultaneously.
Output:
[appHighlight] matched <p> → background set to yellow
[appHover] matched <button> → mouseenter/mouseleave bound
@if evaluated user() → rendered "Welcome back" branch
Best practices
- Prefer the built-in
@if/@for/@switchcontrol flow over legacy*ngIf/*ngForin new code — they are faster and need no imports. - Use attribute directives to encapsulate reusable behavior (tooltips, autofocus, permission checks) instead of duplicating logic across components.
- Keep one responsibility per directive; compose multiple small directives on an element rather than building one that does everything.
- Use
inject()and signalinput()in new directives for cleaner, tree-shakable code. - Use
hostmetadata or@HostBinding/@HostListenerinstead of touchingElementRef.nativeElementdirectly when possible, to stay platform-agnostic. - Always
trackitems in@for(or usetrackBywith*ngFor) to avoid unnecessary DOM re-creation.