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
hostproperty 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
- Write an
appAutofocusdirective that focuses its element after it is created, usingafterNextRender. - Add a
delayinput to the highlight directive. - Close the menu when the user presses the Escape key by adding a
(keydown.escape)host listener.
Quick check
- What is the difference between a component and a directive?
- Which property binds events and properties on the host element?
- Why use a prefix in the selector?
Next, learn to build flexible components with content projection and templates. The directives guide has more.