Skip to main content

Directives (Attribute, Structural, Custom)

Directives are a powerful Angular-specific feature that attach reusable behaviors to DOM elements in component templates. This concept doesn't exist in React and significantly improves code reusability in large-scale development.

1. Three Types of Directives

Angular directives fall into three categories. All directives are defined with the @Directive decorator (or its superset, @Component).

TypeDecoratorRoleExamples
Component directive@ComponentA directive with a template (the UI building block)<app-button>
Structural directive@DirectiveAdds, removes, or reshapes DOM structure*ngIf, *ngFor, @if, @for
Attribute directive@DirectiveChanges appearance or behavior without adding/removing DOM nodesngClass, ngStyle, [appHighlight]

Component Directives

@Component internally extends @Directive. It adds a template (template/templateUrl) and styles (styles/styleUrls) to form the UI building block.

app-button.component.ts
@Component({
selector: 'app-button',
standalone: true,
template: `<button class="btn"><ng-content /></button>`,
})
export class ButtonComponent {}

Structural Directives

The asterisk (*) prefix marks a structural directive. Internally it is desugared into <ng-template>, which controls DOM insertion and removal. Angular 17+ offers the @if / @for control-flow syntax as a modern alternative.

structural-examples.html
<!-- Legacy structural directives -->
<div *ngIf="isLoggedIn">Welcome!</div>
<li *ngFor="let item of items; let i = index">{{ i }}: {{ item.name }}</li>

<!-- Angular 17+ control-flow syntax (recommended) -->
@if (isLoggedIn) {
<div>Welcome!</div>
}
@for (item of items; track item.id) {
<li>{{ item.name }}</li>
}

Attribute Directives

Attribute directives add styles, classes, or event handling to existing elements without changing the DOM structure.

attribute-examples.html
<p [ngClass]="{ 'active': isActive, 'error': hasError }">Text</p>
<p [appHighlight]="'#ffeb3b'">Highlights on hover</p>
Comparison with React

React has no "directive" concept. The closest alternatives are:

  • Structural directives → inline JSX conditions ({condition && <Component />}) and list renders (items.map(...))
  • Attribute directives → custom hooks (useHover, useClickOutside) or HOCs (Higher-Order Components)

The key difference is that Angular directives are applied declaratively in the template.

2. Built-in Attribute Directives

Angular ships several useful attribute directives out of the box. Import CommonModule (or the individual standalone directive) to use them.

ngClass — Dynamic CSS classes

ngClass accepts an object, array, or string.

ngclass-examples.html
<!-- Object syntax (recommended): key = class name, value = boolean -->
<div [ngClass]="{ 'active': isActive, 'disabled': isDisabled, 'error': hasError }">
Object syntax
</div>

<!-- Array syntax: apply multiple classes at once -->
<div [ngClass]="['base-style', isActive ? 'active' : '', sizeClass]">
Array syntax
</div>

<!-- String syntax: simple toggle -->
<div [ngClass]="isActive ? 'active' : 'inactive'">
String syntax
</div>

<!-- For a single class, the [class.name] shorthand is cleaner -->
<div [class.active]="isActive" [class.error]="hasError">
Shorthand (best for simple cases)
</div>
Comparison with React

In React you typically use clsx or classnames:

// React + clsx
import clsx from 'clsx';
<div className={clsx({ active: isActive, disabled: isDisabled, error: hasError })}>
Text
</div>

Angular's ngClass provides the same functionality without a third-party library.

ngStyle — Dynamic inline styles

ngstyle-example.html
<!-- Object syntax for inline styles -->
<div [ngStyle]="{ 'color': textColor, 'font-size.px': fontSize, 'opacity': isVisible ? 1 : 0 }">
Dynamic styles
</div>

<!-- [style.property] shorthand for single properties -->
<div [style.color]="textColor" [style.font-size.px]="fontSize">
Shorthand
</div>

ngModel — Two-way binding

Provides two-way binding for form elements. Requires FormsModule.

simple-form.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
selector: 'app-simple-form',
standalone: true,
imports: [FormsModule],
template: `
<input [(ngModel)]="username" placeholder="Enter username" />
<p>Value: {{ username }}</p>
`,
})
export class SimpleFormComponent {
username = '';
}
How ngModel works

[(ngModel)] is the "banana in a box" syntax. Internally it combines [ngModel]="value" (property binding) and (ngModelChange)="value = $event" (event binding).

This is equivalent to React's value={value} onChange={(e) => setValue(e.target.value)}.

3. Creating Custom Attribute Directives

Custom attribute directives let you attach a shared behavior to any DOM element across multiple components. The example below creates a HighlightDirective that changes the background color on hover.

Generate command

ng generate directive highlight
# or shorthand
ng g d highlight

Implementation

highlight.directive.ts
import {
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
Renderer2,
} from '@angular/core';

@Directive({
selector: '[appHighlight]',
standalone: true,
})
export class HighlightDirective {
// Use the directive selector itself as an input: [appHighlight]="'yellow'"
@Input() appHighlight = 'yellow';

// @HostBinding directly binds a property on the host element
@HostBinding('style.transition') transition = 'background-color 0.3s ease';

constructor(private el: ElementRef, private renderer: Renderer2) {}

@HostListener('mouseenter')
onMouseEnter(): void {
// Renderer2 keeps SSR (server-side rendering) safe
this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', this.appHighlight);
}

@HostListener('mouseleave')
onMouseLeave(): void {
this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');
}
}
app.component.ts (usage)
import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';

@Component({
selector: 'app-root',
standalone: true,
imports: [HighlightDirective],
template: `
<!-- Default color (yellow) -->
<p appHighlight>Hover over me</p>

<!-- Custom color -->
<p [appHighlight]="'#a5d6a7'">Green highlight</p>
<button [appHighlight]="'#ef9a9a'">Red highlight</button>
`,
})
export class AppComponent {}

Why use Renderer2 instead of direct DOM access?

ApproachProsCons
el.nativeElement.style.color = '...'SimpleNot SSR-safe, hard to test
Renderer2.setStyle(...)SSR-safe, testableSlightly more verbose
Comparison with React custom hooks

React custom hooks reuse component state and lifecycle logic. Angular directives attach behavior to DOM elements.

// React: useHover hook manages hover state
function useHover() {
const [isHovered, setIsHovered] = useState(false);
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
el.addEventListener('mouseenter', () => setIsHovered(true));
el.addEventListener('mouseleave', () => setIsHovered(false));
return () => { /* cleanup */ };
}, []);
return { ref, isHovered };
}

// The component must apply the style itself
function MyComponent() {
const { ref, isHovered } = useHover();
return <p ref={ref} style={{ background: isHovered ? 'yellow' : '' }}>Text</p>;
}

With Angular's directive, just write appHighlight in the template — the logic and styling are encapsulated together.

4. Creating Custom Structural Directives

Structural directives use TemplateRef and ViewContainerRef to control whether elements are added to or removed from the DOM.

ClassRole
TemplateRefHolds a reference to the <ng-template> where *appXxx is applied
ViewContainerRefContainer that renders the template into the DOM

Example: role-based permission directive

has-role.directive.ts
import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from './auth.service';

@Directive({
selector: '[appHasRole]',
standalone: true,
})
export class HasRoleDirective implements OnInit {
private requiredRole = '';

@Input() set appHasRole(role: string) {
this.requiredRole = role;
this.updateView();
}

constructor(
private templateRef: TemplateRef<any>,
private vcr: ViewContainerRef,
private authService: AuthService,
) {}

ngOnInit(): void {
this.updateView();
}

private updateView(): void {
this.vcr.clear(); // Clear before re-evaluating
if (this.authService.hasRole(this.requiredRole)) {
// Render the template into the DOM
this.vcr.createEmbeddedView(this.templateRef);
}
}
}
auth.service.ts (simplified)
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
private currentUserRoles = ['user', 'editor'];

hasRole(role: string): boolean {
return this.currentUserRoles.includes(role);
}
}
Usage
<!-- Only shown to users with 'admin' role -->
<div *appHasRole="'admin'">
<h3>Admin Menu</h3>
<ul>
<li>User Management</li>
<li>System Settings</li>
</ul>
</div>

<!-- Shown to 'editor' role -->
<button *appHasRole="'editor'">Edit Article</button>

<!-- Visible to everyone -->
<p>Public content</p>
Comparison with React

React handles conditional rendering with inline JSX expressions:

// React: inline conditional rendering
function AdminMenu() {
return (
<>
{hasRole('admin') && (
<div>
<h3>Admin Menu</h3>
</div>
)}
{hasRole('editor') && <button>Edit Article</button>}
</>
);
}

// Or wrap in a custom component
function RequireRole({ role, children }) {
return hasRole(role) ? children : null;
}

Angular's *appHasRole keeps permission logic completely out of components and applies it declaratively in templates — a significant maintainability advantage for large teams.

5. HostDirective (Directive Composition)

Since Angular 15, the hostDirectives property enables directive composition: a component or directive can incorporate the functionality of other directives.

Basic usage

button.component.ts
import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
import { TooltipDirective } from './tooltip.directive';

@Component({
selector: 'app-button',
standalone: true,
// Compose directives via hostDirectives
hostDirectives: [
{
directive: HighlightDirective,
// Expose inputs/outputs to the parent (aliases supported)
inputs: ['appHighlight: color'],
},
{
directive: TooltipDirective,
inputs: ['appTooltip: tooltip'],
outputs: ['tooltipShown'],
},
],
template: `<button class="btn"><ng-content /></button>`,
})
export class ButtonComponent {}
Usage
<!-- Pass values to composed directives through aliased inputs -->
<app-button color="#b3e5fc" tooltip="Click to submit">Submit</app-button>

Composition vs inheritance

AspectComposition (hostDirectives)Inheritance
Combining multiple featuresEasy — list multiple directivesHard — single inheritance
Loose couplingHigh — dependencies are explicitLow — parent changes ripple down
TestabilityHigh — each directive tested independentlyLow — whole hierarchy needed for setup
Angular recommendation✅ Recommended❌ Not recommended
Comparison with React

React achieves similar composition through HOC chains or combined hooks:

// React: HOC composition
const EnhancedButton = withTooltip(withHighlight(Button));

// Or combining hooks
function ButtonComponent({ color, tooltip }) {
const highlightProps = useHighlight(color);
const tooltipProps = useTooltip(tooltip);
return <button {...highlightProps} {...tooltipProps}>Submit</button>;
}

hostDirectives is a framework-level composition API that eliminates much of the boilerplate required in React.

6. Directives vs React Hooks

Both mechanisms exist for logic reuse, but they take very different approaches.

AspectAngular DirectivesReact Hooks
How appliedWritten as an attribute in the HTML templateCalled inside a component function
TargetActs directly on a DOM elementActs on component state, lifecycle, and side effects
DOM manipulationDirect and safe via ElementRef / Renderer2Indirect via useRef + useEffect
Reuse granularityDeclaratively applied to any DOM elementLogic is mixed into component functions
Template impactStructural directives control DOM presenceJSX conditionals and list expressions
Dependency injectionServices injected via Angular DIContext API or external stores
TestingTest with TestBed and a host elementTest with renderHook / RTL independently

Summary

  • Angular directives: DOM-centric and declarative. Simply add an attribute to the template to attach behavior. Easy to standardize across large teams.
  • React hooks: Component-logic-centric. Excel at encapsulating state, side effects, and context. Flexible to compose as plain JavaScript functions.

7. Practical Patterns

a. Tooltip directive

A lightweight hover tooltip using @HostListener and dynamic element creation.

tooltip.directive.ts
import {
Directive,
ElementRef,
HostListener,
Input,
OnDestroy,
Renderer2,
} from '@angular/core';

@Directive({
selector: '[appTooltip]',
standalone: true,
})
export class TooltipDirective implements OnDestroy {
@Input() appTooltip = '';

private tooltipEl: HTMLElement | null = null;

constructor(private el: ElementRef, private renderer: Renderer2) {}

@HostListener('mouseenter')
onMouseEnter(): void {
this.tooltipEl = this.renderer.createElement('span');
this.renderer.appendChild(
this.tooltipEl,
this.renderer.createText(this.appTooltip),
);
this.renderer.setStyle(this.tooltipEl, 'position', 'absolute');
this.renderer.setStyle(this.tooltipEl, 'background', '#333');
this.renderer.setStyle(this.tooltipEl, 'color', '#fff');
this.renderer.setStyle(this.tooltipEl, 'padding', '4px 8px');
this.renderer.setStyle(this.tooltipEl, 'border-radius', '4px');
this.renderer.setStyle(this.tooltipEl, 'font-size', '12px');
this.renderer.setStyle(this.tooltipEl, 'white-space', 'nowrap');
this.renderer.setStyle(this.tooltipEl, 'z-index', '1000');
this.renderer.setStyle(this.el.nativeElement, 'position', 'relative');
this.renderer.appendChild(this.el.nativeElement, this.tooltipEl);
}

@HostListener('mouseleave')
onMouseLeave(): void {
this.removeTooltip();
}

ngOnDestroy(): void {
this.removeTooltip();
}

private removeTooltip(): void {
if (this.tooltipEl) {
this.renderer.removeChild(this.el.nativeElement, this.tooltipEl);
this.tooltipEl = null;
}
}
}
Usage
<button [appTooltip]="'Click to save'">Save</button>
<span [appTooltip]="'This field is required'">Username *</span>

b. Click-outside detection directive

Detects clicks outside the host element — useful for closing dropdowns and modals.

click-outside.directive.ts
import { Directive, ElementRef, EventEmitter, HostListener, Output } from '@angular/core';

@Directive({
selector: '[appClickOutside]',
standalone: true,
})
export class ClickOutsideDirective {
@Output() clickOutside = new EventEmitter<void>();

constructor(private elementRef: ElementRef) {}

// Listen to the entire document's click event
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
const target = event.target as Node;
if (!this.elementRef.nativeElement.contains(target)) {
this.clickOutside.emit();
}
}
}
Usage
<div class="dropdown" (appClickOutside)="closeDropdown()">
<button (click)="toggleDropdown()">Menu ▼</button>
@if (isOpen) {
<ul class="dropdown-menu">
<li>Item 1</li>
<li>Item 2</li>
</ul>
}
</div>

c. Auto-focus directive

Automatically focuses an element when it is inserted into the DOM. Useful for modals and search forms.

auto-focus.directive.ts
import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core';

@Directive({
selector: '[appAutoFocus]',
standalone: true,
})
export class AutoFocusDirective implements AfterViewInit {
// Set to false to disable auto-focus conditionally
@Input() appAutoFocus = true;

constructor(private el: ElementRef<HTMLElement>) {}

ngAfterViewInit(): void {
if (this.appAutoFocus) {
// Use a microtask to focus after any animations complete
Promise.resolve().then(() => this.el.nativeElement.focus());
}
}
}
Usage
<!-- Auto-focus the search field when the modal opens -->
<input appAutoFocus type="text" placeholder="Search..." />

<!-- Conditionally enable auto-focus -->
<input [appAutoFocus]="shouldFocus" type="email" />

d. Permission control directive

An extension of the *appHasRole pattern from Section 4 that accepts multiple permission strings.

has-permission.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
import { AuthService } from './auth.service';

@Directive({
selector: '[appHasPermission]',
standalone: true,
})
export class HasPermissionDirective {
@Input() set appHasPermission(permission: string | string[]) {
const permissions = Array.isArray(permission) ? permission : [permission];
this.vcr.clear();
if (permissions.some(p => this.authService.hasPermission(p))) {
this.vcr.createEmbeddedView(this.templateRef);
}
}

constructor(
private templateRef: TemplateRef<any>,
private vcr: ViewContainerRef,
private authService: AuthService,
) {}
}
Usage
<!-- Single permission -->
<button *appHasPermission="'article:delete'">Delete Article</button>

<!-- Multiple permissions (shown if user has ANY of them) -->
<div *appHasPermission="['article:edit', 'article:publish']">
Edit / Publish Menu
</div>

e. Numbers-only input directive

Blocks non-numeric keyboard input in a text field.

numbers-only.directive.ts
import { Directive, HostListener } from '@angular/core';

@Directive({
selector: '[appNumbersOnly]',
standalone: true,
})
export class NumbersOnlyDirective {
@HostListener('keypress', ['$event'])
onKeyPress(event: KeyboardEvent): void {
const allowedKeys = ['Backspace', 'Tab', 'Enter', 'ArrowLeft', 'ArrowRight', 'Delete'];
if (allowedKeys.includes(event.key)) {
return; // Allow control keys
}
if (!/^\d$/.test(event.key)) {
event.preventDefault(); // Block non-digits
}
}

@HostListener('paste', ['$event'])
onPaste(event: ClipboardEvent): void {
const pastedText = event.clipboardData?.getData('text') ?? '';
if (!/^\d+$/.test(pastedText)) {
event.preventDefault(); // Block non-numeric paste
}
}
}
Usage
<!-- Postal codes, phone numbers, etc. -->
<input appNumbersOnly type="text" placeholder="Zip code (digits only)" maxlength="5" />
<input appNumbersOnly type="text" placeholder="Phone number (digits only)" />
Summary

All practical directives above are implemented as standalone (standalone: true). Keeping each directive in its own file makes testing and reuse straightforward. Place shared directives in a shared/directives/ directory and re-export them through a barrel file (index.ts) as a best practice.