Reader Stacks

Creating Custom Directives in Angular

An attribute directive changes the behavior or appearance of the element it's placed on — the same tool that powers ngClass and ngStyle, applied to your own logic.

Angular has three kinds of directives: components (a directive with a template), structural directives like *ngIf and *ngFor (which add or remove elements from the DOM), and attribute directives — the kind covered here — which change the appearance or behavior of an existing element without adding or removing it.

1. A basic highlight directive

ng generate directive highlight
import { Directive, ElementRef, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
})
export class HighlightDirective {
  private el = inject(ElementRef);

  constructor() {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }
}
<p appHighlight>This text has a yellow background.</p>

ElementRef gives direct access to the host DOM element — reaching for it directly like this (rather than through Angular's Renderer2) is fine for a build that only ever runs in a browser, but it bypasses Angular's platform abstraction, which matters if the app also needs to run server-side (SSR) or in a Web Worker, where nativeElement.style may not exist.

2. Reacting to events

import { Directive, ElementRef, HostListener, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
})
export class HighlightDirective {
  private el = inject(ElementRef);

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = 'yellow';
  }

  @HostListener('mouseleave')
  onMouseLeave(): void {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

@HostListener attaches an event listener to whichever element the directive is applied to, without the directive needing to know that element's tag name or selector in advance — the same directive works identically on a <div>, a <p>, or a custom component.

3. Accepting an input

import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
})
export class HighlightDirective {
  @Input('appHighlight') highlightColor = 'yellow';

  private el = inject(ElementRef);

  @HostListener('mouseenter')
  onMouseEnter(): void {
    this.el.nativeElement.style.backgroundColor = this.highlightColor;
  }
}
<p [appHighlight]="'lightblue'">Custom color</p>

Naming the @Input the same as the directive's own selector (appHighlight) lets it be set inline on the same attribute that applies the directive, rather than requiring a second, separately-named binding — this is exactly the pattern ngModel and routerLink use internally.

4. When to reach for a directive instead of a component

If the goal is reusable DOM structure — markup with its own template — that's a component. If the goal is reusable behavior attached to markup that already exists elsewhere (a tooltip, a click-outside handler, an autofocus behavior, a permission-based show/hide), an attribute directive is the better fit: it composes onto any existing element instead of wrapping it in a new one.

Topics: Developer Productivity