Building Custom Directives in Angular

Hero image for Building Custom Directives in Angular. Image by Tomasz Zielonka.
Hero image for 'Building Custom Directives in Angular.' Image by Tomasz Zielonka.

Angular directives are easy to underuse and easy to overuse. On one side, teams sometimes push all DOM behaviour into components even when the behaviour is clearly reusable across several templates. On the other, they can create directives so eagerly that the template layer becomes full of tiny abstractions no one enjoys reading.

The useful middle ground is simple: directives are strongest when they encapsulate DOMoriented behaviour that does not deserve an entire component. If we keep that boundary clear, custom directives become a very practical way to improve reuse without making the codebase harder to follow.


When a Directive is the Right Abstraction

Directives are a good fit for behaviour such as focus management, visibility tracking, keyboard shortcuts, or repeated hostelement decoration. They are not a good fit for rendering a chunk of UI, because that is what components already do well.

Some teams assume that directives are mostly a legacy Angular feature left behind by newer APIs. They are still highly relevant. The question is only whether the behaviour belongs to an existing host element or deserves its own rendered structure.

That distinction matters because Angular gives us three different tools for a reason:

  • components for rendered UI
  • attribute directives for hostelement behaviour
  • structural directives for DOM shape

Once we confuse those roles, templates become much harder to reason about.


A Practical Standalone Directive

import {  Directive,  ElementRef,  HostListener,  Input,  Renderer2,  inject,} from '@angular/core';@Directive({  selector: '[appHighlightOnHover]',  standalone: true,})export class HighlightOnHoverDirective {  @Input() appHighlightOnHover = '#fff6cc';  private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);  private readonly renderer = inject(Renderer2);  @HostListener('mouseenter')  handleMouseEnter(): void {    this.renderer.setStyle(      this.elementRef.nativeElement,      'backgroundColor',      this.appHighlightOnHover,    );  }  @HostListener('mouseleave')  handleMouseLeave(): void {    this.renderer.removeStyle(      this.elementRef.nativeElement,      'backgroundColor',    );  }}

This is intentionally small. The directive owns one behaviour, stays close to the DOM, and can be imported wherever the host interaction is needed. That keeps it maintainable and makes the decision to create it easy to defend.


Why Renderer2 Still Matters

It can be tempting to reach straight for nativeElement.style because it looks shorter. Angular's own APIs are usually the safer default for hostelement updates. Renderer2 makes the intent clearer and keeps the directive aligned with Angular's rendering model instead of treating the browser DOM as the only environment that matters.

That is not about ceremony for its own sake. It is about writing directives that behave more predictably across the framework's own abstractions.


Keep the Directive Surface Area Narrow

Directives become awkward when they begin coordinating too many concerns. If a directive is handling API calls, global state, analytics, and rendering concerns all at once, it probably wants to be broken apart. The directive layer should remain a focused behavioural seam, not a hidden application framework inside the template.

Selector design matters too. Clear attribute names help readability, and limited inputs make the behaviour easier to reason about. A directive that exposes ten loosely related inputs is often a sign that the abstraction is too broad.

This is where good naming earns its keep. [appHighlightOnHover] tells us what happens and where it happens. A vague selector such as [interactive] tells us almost nothing and usually ages badly.


Test the Behaviour, Not the Decorator

Directives are highly testable when they own observable host behaviour and little else. In practice, that means the test should mount a host component, apply the directive, trigger the interaction, and assert on the host element result.

That is a more useful test than one trying to inspect Angular metadata itself. We do not really care that @HostListener exists. We care that the background colour changes on hover and is removed afterwards.

This is one of the nicest things about a wellscoped directive. The contract is visible in the DOM, so the test can stay small and meaningful.


Keeping Directives Useful

Custom directives remain maintainable when they avoid broad side effects and keep their selectors explicit. They scale well when the behaviour really is reused across several components, because each additional use then reduces duplication rather than introducing confusion.

This is where good directive design pays off. A small behavioural abstraction can quietly simplify a large UI if it is precise enough.

The official Angular guides below are the clearest references for the APIs touched on here:


Make Directives Easy to Spot in Templates

One practical rule helps here: if someone cannot tell what a directive is doing from the selector name and a quick glance at the inputs, the abstraction is probably too clever. Directives work best when the template still reads cleanly and the behaviour feels narrow enough to trust.

That is also why naming matters. A selector should describe the behaviour we are attaching, not the implementation detail behind it. Good directives feel obvious at the call site, which is a large part of what makes them maintainable.


Wrapping up

Custom directives are most effective when they stay close to the DOM problem they are solving. The moment they start carrying broader application behaviour, the abstraction usually becomes harder to justify.

Key Takeaways

  • Directives are best for reusable DOM behaviour that belongs on an existing element.
  • Attribute directives, structural directives, and components solve different problems.
  • A narrow responsibility keeps directive APIs readable and maintainable.
  • Good directives reduce duplication without hiding too much application logic in templates.

Custom directives remain one of Angular's most useful architectural tools when we apply them selectively. The strongest ones are small, obvious, and easy to reuse, which is exactly why they keep paying dividends over time.


Categories:

  1. Angular
  2. Development
  3. Front‑End Development
  4. Guides
  5. JavaScript