Attribute directives add behavior to an existing host. Structural directives create and remove embedded views. Angular built-in control-flow blocks are separate template syntax. Current directive APIs include signal inputs, host metadata, TemplateRef, ViewContainerRef, and hostDirectives.
During template creation, Angular matches directive selectors against each host element and creates one instance for every match. That instance can expose inputs and outputs, bind host properties or events, provide dependencies, and run lifecycle cleanup.
A component is a directive with a template. An attribute directive changes an existing element or component. A structural directive controls an embedded template and therefore changes which DOM nodes exist. These categories describe ownership, not visual appearance.
The built-in @if, @for, and @switch blocks are Angular template syntax, not directives. They need no import. Use them for normal conditions, iteration, and branching; create a structural directive only for reusable rendering behavior that those blocks do not express.
| Tool | Ownership | Use it for |
|---|---|---|
| Component | Own host plus template | A card, dialog, page, form field, or visible UI unit |
| Attribute directive | Behavior on an existing host | Focus, keyboard, permissions, interaction, or reusable state styling |
| Structural directive | An ng-template and embedded views | Application-specific conditional or contextual rendering |
| @if, @for, @switch | Built-in template control flow | Normal conditions, loops, and alternatives |
| Class, style, or property binding | One value on one element | A clear local change with no reusable behavior |
The selector decides where Angular creates the directive. Attribute selectors such as [appHighlight] are the normal choice because they add behavior without claiming an element name. Prefix application selectors to avoid collisions with native attributes and library directives.
Signal inputs configure each instance and update when parent bindings change. Inputs can be optional, required, aliased, or transformed. Use a required input only when the directive has no useful default.
Use the decorator host object for host properties, ARIA or data attributes, classes, styles, CSS custom properties, and events. Angular 22 recommends host metadata over @HostBinding and @HostListener, which remain mainly for compatibility.
appHighlight accepts its color through an aliased signal input. Host metadata applies the class, style, data attribute, and pointer listeners without nativeElement mutation or tag-specific assumptions.
import { Directive, computed, input, signal } from '@angular/core';
@Directive({
selector: '[appHighlight]',
host: {
'[class.is-highlighted]': 'hovered()',
'[style.background-color]': 'background()',
'[attr.data-highlighted]': 'hovered() ? "true" : null',
'(mouseenter)': 'show()',
'(mouseleave)': 'hide()'
}
})
export class HighlightDirective {
readonly color = input('#fff3a3', { alias: 'appHighlight' });
readonly hovered = signal(false);
readonly background = computed(() =>
this.hovered() ? this.color() : 'transparent'
);
show(): void { this.hovered.set(true); }
hide(): void { this.hovered.set(false); }
}
The appHighlight selector value configures color while the class uses the descriptive property name color. Returning null removes the data attribute.
import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
@Component({
selector: 'app-help',
imports: [HighlightDirective],
template: `
<p appHighlight="#dff7ff">Hover for the custom color.</p>
<button appHighlight>Default highlight</button>
`
})
export class HelpComponent {}
Standalone is the default for current Angular declarations. The directive still belongs in the consuming component imports.
Static host entries use plain keys such as role. Dynamic entries use binding syntax such as [attr.aria-expanded], [class.open], [style.width.px], or (keydown.escape). Update accessibility state together with visual state.
A consumer may bind the same host property. Avoid surprising collisions by documenting host behavior and exposing configuration. If Angular bindings cannot express an imperative write, isolate it, account for server rendering, and use Renderer2 where appropriate.
NgClass adds and removes multiple CSS classes from a string, array, or object. NgStyle applies a map of inline styles. Both come from @angular/common and must be imported by a standalone component that uses them.
Prefer a direct class binding for one boolean class and a direct style binding for one value. Use NgClass or NgStyle when several values form one readable configuration object. Keep durable presentation rules in CSS classes; reserve NgStyle for values calculated at runtime.
NgModel is also an attribute directive, but its form contract belongs in the forms lesson. NgOptimizedImage is a specialized image directive. Built-in directives use the same public Angular APIs available to custom directives.
| Requirement | Preferred syntax |
|---|---|
| Toggle one class | [class.selected]="selected()" |
| Set one style | [style.width.px]="width()" |
| Apply several conditional classes | [ngClass]="classMap()" |
| Apply several calculated styles | [ngStyle]="styleMap()" |
| Reuse interaction across templates | A custom attribute directive |
import { NgClass } from '@angular/common';
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-sync-status',
imports: [NgClass],
template: `
<p
[class.is-online]="online()"
[style.opacity]="online() ? 1 : 0.65"
[ngClass]="statusClasses()"
>
{{ online() ? 'Connected' : 'Offline' }}
</p>
`
})
export class SyncStatusComponent {
readonly online = signal(false);
readonly warning = signal(true);
readonly statusClasses = computed(() => ({
'has-warning': this.warning(),
'is-compact': true
}));
}
The one online class and opacity use direct bindings. Related warning and density classes share one NgClass object.
Keep each directive responsible for one host behavior. Name inputs after configuration, emit outputs for meaningful behavior events, and inject services only when the behavior depends on application policy. Unrelated page state belongs in a component or service.
Directives participate in dependency injection and lifecycle hooks. They can provide a token, inject a peer directive, and clean up resources with DestroyRef. Use takeUntilDestroyed for observable subscriptions started by the directive.
exportAs publishes a directive instance to a template reference variable. Use it when a template genuinely needs a small imperative API. Inputs and outputs remain the default because they keep data flow explicit.
TooltipDirective requires tooltip text, emits a close event, and exports open and close methods. Its host bindings expose interaction state in the DOM.
import { Directive, input, output, signal } from '@angular/core';
@Directive({
selector: '[appTooltip]',
exportAs: 'appTooltip',
host: {
'[attr.aria-label]': 'text()',
'[attr.data-tooltip-open]': 'openState() ? "true" : null',
'(focus)': 'open()',
'(blur)': 'close()',
'(keydown.escape)': 'close()'
}
})
export class TooltipDirective {
readonly text = input.required<string>({ alias: 'appTooltip' });
readonly closed = output<void>();
readonly openState = signal(false);
open(): void { this.openState.set(true); }
close(): void {
if (!this.openState()) return;
this.openState.set(false);
this.closed.emit();
}
}
<button
appTooltip="Saves the current draft"
#tip="appTooltip"
(closed)="recordTooltipClose()"
(click)="tip.close()"
>
Save
</button>
The variable resolves to TooltipDirective because exportAs matches appTooltip. A large exported API usually means the behavior should become a component.
ElementRef is useful for focus, measurement, observers, or a DOM-only library. It is not the first tool for classes, styles, attributes, or events because host bindings express those operations directly.
Never insert untrusted HTML through nativeElement.innerHTML. Manual writes can bypass Angular security behavior. Code that may run during server rendering must not assume window, document, layout measurements, or browser-only constructors exist.
A structural directive is attached to an ng-template and decides when or how many embedded views Angular creates. TemplateRef describes the content; ViewContainerRef represents the insertion location.
The asterisk form is shorthand. Angular rewrites an element carrying *appUnless into an ng-template carrying the directive, with the original element inside. One shorthand expression creates one implicit ng-template, so only one structural directive can occupy an element. Use ng-container layers to make nesting explicit without adding DOM wrappers.
Use @if and @for for normal conditions and lists. NgIf, NgFor, and NgSwitch are deprecated in Angular 22. Create a structural directive for domain behavior such as permission-aware rendering, data selection with context, or view orchestration.
The appUnless setter creates or clears its embedded view when the condition changes. The hasView flag prevents duplicate content when repeated values have the same truthiness.
import {
Directive, Input, TemplateRef, ViewContainerRef, inject
} from '@angular/core';
@Directive({ selector: '[appUnless]' })
export class UnlessDirective {
private readonly template = inject(TemplateRef<unknown>);
private readonly container = inject(ViewContainerRef);
private hasView = false;
@Input()
set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.container.createEmbeddedView(this.template);
this.hasView = true;
} else if (condition && this.hasView) {
this.container.clear();
this.hasView = false;
}
}
}
<p *appUnless="accountLocked()">
Account actions are available.
</p>
<ng-template [appUnless]="accountLocked()">
<p>Account actions are available.</p>
</ng-template>
The directive lives on ng-template after expansion. It creates the paragraph only while accountLocked() is false.
For selector [select], *select="let item; from: source" maps from to selectFrom and maps let item to the context property $implicit. Prefix related inputs with the selector so this transformation stays predictable.
Pass exported values in the context object supplied to createEmbeddedView. Add ngTemplateContextGuard for typed generic context and an ngTemplateGuard_input member when the directive narrows a bound expression.
| Shorthand | Expanded meaning |
|---|---|
| *select="source" | [select]="source" on ng-template |
| from: source | [selectFrom]="source" |
| let item | let-item="$implicit" |
| value as local | A local variable for an exported context value |
The directive composition API applies reusable behavior through hostDirectives on a component or another directive. Angular applies host directives statically at compile time to the same host element, and ignores their selectors in this role.
Host directive inputs and outputs are private by default. Expose only members that belong in the composed public API, and alias them when needed. A host directive must be standalone; current Angular declarations are standalone by default unless standalone: false is set.
Host directives run construction, lifecycle, and host bindings before the type that composes them. The composing component can therefore override host bindings intentionally. Composition is static, so it cannot add or remove behavior at runtime.
import { Component, Directive, input, output } from '@angular/core';
@Directive({
host: {
'role': 'menu',
'[attr.data-menu-id]': 'menuId()',
'(keydown.escape)': 'close()'
}
})
export class MenuBehavior {
readonly menuId = input.required<string>();
readonly menuClosed = output<void>();
close(): void { this.menuClosed.emit(); }
}
@Component({
selector: 'admin-menu',
template: `<button role="menuitem">Users</button>`,
hostDirectives: [{
directive: MenuBehavior,
inputs: ['menuId: behaviorId'],
outputs: ['menuClosed: closed']
}]
})
export class AdminMenu {}
Consumers bind behaviorId and closed on admin-menu. The MenuBehavior selector is irrelevant because hostDirectives applies it explicitly.
Use a local binding for a single, readable host value. Create a directive when a named behavior repeats, needs dependency injection or lifecycle cleanup, or must be composed into several hosts.
A directive should rely on its public contract, not component private fields or undocumented child markup. If behavior needs substantial markup, projection, layout, and many interaction states, a component gives it a clearer boundary.
A permission directive can hide or disable controls, but authorization must still be enforced by the server. Interface behavior is never a security boundary.
| Situation | Choose | Why |
|---|---|---|
| One element toggles one class | Class binding | The local behavior is already clear. |
| Hosts share focus and keyboard rules | Attribute directive | The behavior has a reusable host contract. |
| A feature owns markup and structure | Component | It needs a view boundary. |
| Normal condition or list | @if or @for | Built-in control flow directly expresses it. |
| Rendering exports reusable context | Structural directive | It needs a named embedded-view abstraction. |
| A component always needs host behaviors | hostDirectives | Static composition avoids repeated annotations. |
Test an attribute directive through a small host component. This covers selector matching, input binding, host events, and rendered DOM together. Include default and custom input cases.
For structural directives, assert embedded content before and after input changes. For composition, test public aliases and resulting host behavior rather than private implementation details.
An unknown-property error usually means a missing import or mismatched selector or input. Duplicate views point to unmanaged ViewContainerRef creation. Stale behavior after navigation points to missing cleanup.
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HighlightDirective } from './highlight.directive';
@Component({
imports: [HighlightDirective],
template: `<p appHighlight="#dff7ff">Read me</p>`
})
class TestHost {}
it('updates the host through pointer events', () => {
const fixture = TestBed.createComponent(TestHost);
fixture.detectChanges();
const paragraph = fixture.debugElement.query(By.css('p'));
paragraph.triggerEventHandler('mouseenter');
fixture.detectChanges();
expect(paragraph.nativeElement.classList.contains('is-highlighted'))
.toBeTrue();
paragraph.triggerEventHandler('mouseleave');
fixture.detectChanges();
expect(paragraph.nativeElement.classList.contains('is-highlighted'))
.toBeFalse();
});
The test observes public DOM effects. Inject the directive from the debug element only when its class API needs direct verification.
| Symptom | Likely cause | Check |
|---|---|---|
| Can't bind to appHighlight | Missing import or wrong alias | Import the directive; compare selector and input alias. |
| Constructor never runs | Selector mismatch | Inspect template spelling and selector form. |
| Host state is stale | Binding reads wrong state | Trace the signal or input and host expression. |
| Content appears twice | View created without reuse logic | Track view existence and repeated inputs. |
| Composed input is unknown | It was not exposed | Add it to hostDirectives and verify the alias. |
| SSR fails on window or layout | Browser-only access | Use a platform-aware boundary or render callback. |
| Behavior survives navigation | Resource was not released | Register DestroyRef cleanup and test destruction. |
Try this next
0 of 2 completed
No. They are built-in template control-flow blocks and require no directive import. Use them for ordinary conditions, iteration, and branching; NgIf and NgFor are deprecated for new code. A custom structural directive remains useful for reusable application-specific rendering behavior. Because asterisk shorthand creates one implicit ng-template, place multiple structural behaviors on nested ng-container elements.
Use a component when it owns markup and a view boundary. Use an attribute directive when existing markup gains reusable host behavior. Prefer a direct template binding when the change is local and has no reusable behavior.
Prefer host bindings for normal properties, attributes, classes, styles, and events. Direct nativeElement mutation couples code to a browser DOM and complicates rendering, testing, and security, so reserve focused native access or Renderer2 for genuinely imperative work. hostDirectives members stay private by default; explicitly expose or alias only the inputs and outputs consumers need.
Explore 500+ free tutorials across 20+ languages and frameworks.