Angular Custom Directives: Attribute Directives and Host Bindings

Angular Tutorial: Beginner to Advanced · 14. Directives and dynamic UI · 3 min read · Updated

A component has a template. A directive adds behaviour to an element that already exists. Directives are perfect for small reusable jobs: auto-focus, tooltips, click-outside, permissions and drag handles.

What you will learn

  • How to write an attribute directive
  • How the host property binds properties, classes and events
  • How to give a directive inputs and clean up after it

A highlight directive

// src/app/directives/highlight.ts
import { Directive, input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  host: {
    '[style.backgroundColor]': 'active ? color() : null',
    '(mouseenter)': 'active = true',
    '(mouseleave)': 'active = false',
  },
})
export class Highlight {
  // The attribute itself carries the value: appHighlight="gold"
  readonly color = input('yellow', { alias: 'appHighlight' });

  protected active = false;
}

Use it in any template:

<p appHighlight>Yellow on hover</p>
<p appHighlight="lightgreen">Green on hover</p>

The host object is the modern way to bind to the element the directive sits on:

  • '[style.backgroundColor]' binds a property or style.
  • '(mouseenter)' listens to an event.
  • '[class.active]' toggles a class.
  • '[attr.aria-expanded]' sets an attribute.

A click-outside directive

A frequent need: close a menu when the user clicks anywhere else.

// src/app/directives/click-outside.ts
import { Directive, ElementRef, inject, output } from '@angular/core';

@Directive({
  selector: '[appClickOutside]',
  host: {
    '(document:click)': 'onDocumentClick($event)',
  },
})
export class ClickOutside {
  private readonly element = inject<ElementRef<HTMLElement>>(ElementRef);

  readonly clickOutside = output<void>();

  protected onDocumentClick(event: MouseEvent): void {
    const target = event.target as Node | null;
    if (target && !this.element.nativeElement.contains(target)) {
      this.clickOutside.emit();
    }
  }
}

'(document:click)' listens on document, and Angular removes the listener when the element is destroyed, so you do not need manual cleanup for host listeners.

Use both in a dropdown

// src/app/directives/menu-demo.ts
import { Component, signal } from '@angular/core';
import { ClickOutside } from './click-outside';
import { Highlight } from './highlight';

@Component({
  selector: 'app-menu-demo',
  imports: [ClickOutside, Highlight],
  template: `
    <div class="wrap" (appClickOutside)="open.set(false)" appClickOutside>
      <button type="button" [attr.aria-expanded]="open()" (click)="open.set(!open())">Menu</button>
      @if (open()) {
        <ul role="menu">
          <li role="menuitem" appHighlight="#e8f5e9">Profile</li>
          <li role="menuitem" appHighlight="#e8f5e9">Settings</li>
          <li role="menuitem" appHighlight="#ffebee">Sign out</li>
        </ul>
      }
    </div>
  `,
})
export class MenuDemo {
  protected readonly open = signal(false);
}

Look at the first line: (appClickOutside)="..." listens to the directive's output, and the plain appClickOutside attribute attaches the directive.

// src/app/app.ts
import { Component } from '@angular/core';
import { MenuDemo } from './directives/menu-demo';

@Component({
  imports: [MenuDemo],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
  <h1>Directives</h1>
  <app-menu-demo />
</main>

Directive or component?

  • If you need your own template, use a component.
  • If you only change behaviour or look of an existing element, use a directive.
  • Prefix selectors (app...) so they never collide with HTML attributes or other libraries.

Try it yourself

  1. Write an appAutofocus directive that focuses its element after it is created, using afterNextRender.
  2. Add a delay input to the highlight directive.
  3. Close the menu when the user presses the Escape key by adding a (keydown.escape) host listener.

Quick check

  1. What is the difference between a component and a directive?
  2. Which property binds events and properties on the host element?
  3. Why use a prefix in the selector?

Next, learn to build flexible components with content projection and templates. The directives guide has more.

Frequently asked questions

Should I use @HostListener and @HostBinding?

They still work, but the host property is the recommended style in new code and keeps bindings in one place.

What about structural directives like *ngIf?

New templates use @if and @for instead. Custom structural directives are rare today, and ng-template with a template outlet covers most cases.