Skip to main content
angular-directives Create custom directives in Angular v20+ for DOM manipulation and behavior extension. Use for attribute directives that modify element behavior/appearance, structural directives for portals/overlays, and host directives for composition. Triggers on creating reusable DOM behaviors, extending element functionality, or composing behaviors across components. Note - use native @if/@for/@switch for control flow, not custom structural directives.
Jump to install Skills Marketplace Discover and explore AI skills built by the community.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Copy promptShow prompt details A direct command skips the review prompt. Inspect the source before running it.
npx skills add https://github.com/zard-ui/zardui --skill angular-directivesThe command stays on one line. Scroll horizontally to inspect it before copying.
Prefer a local copy? Download the files currently available to SkillsMP.
Download Zip Downloading... More from this repository
Related occupations SOC
Based on SOC occupation classification
name angular-directives description Create custom directives in Angular v20+ for DOM manipulation and behavior extension. Use for attribute directives that modify element behavior/appearance, structural directives for portals/overlays, and host directives for composition. Triggers on creating reusable DOM behaviors, extending element functionality, or composing behaviors across components. Note - use native @if/@for/@switch for control flow, not custom structural directives.
Angular Directives
Create custom directives for reusable DOM manipulation and behavior in Angular v20+.
Attribute Directives
Modify the appearance or behavior of an element:
import { Directive , input, effect, inject, ElementRef } from '@angular/core' ;
@Directive ({
selector : '[appHighlight]' ,
})
export class Highlight {
private el = inject (ElementRef <HTMLElement >);
color = input ('yellow' , { alias : 'appHighlight' });
constructor ( ) {
effect (() => {
this . . . . = . ();
});
}
}
el
nativeElement
style
backgroundColor
this
color
Using host Property Prefer host over @HostBinding/@HostListener:
@Directive ({
selector : '[appTooltip]' ,
host : {
'(mouseenter)' : 'show()' ,
'(mouseleave)' : 'hide()' ,
'[attr.aria-describedby]' : 'tooltipId' ,
},
})
export class Tooltip {
text = input.required <string >({ alias : 'appTooltip' });
position = input<'top' | 'bottom' | 'left' | 'right' >('top' );
tooltipId = `tooltip-${crypto.randomUUID()} ` ;
private tooltipEl : HTMLElement | null = null ;
private el = inject (ElementRef <HTMLElement >);
show ( ) {
this .tooltipEl = document .createElement ('div' );
this .tooltipEl .id = this .tooltipId ;
this .tooltipEl .className = `tooltip tooltip-${this .position()} ` ;
this .tooltipEl .textContent = this .text ();
this .tooltipEl .setAttribute ('role' , 'tooltip' );
document .body .appendChild (this .tooltipEl );
this .positionTooltip ();
}
hide ( ) {
this .tooltipEl ?.remove ();
this .tooltipEl = null ;
}
private positionTooltip ( ) {
}
}
Class and Style Manipulation @Directive ({
selector : '[appButton]' ,
host : {
'class' : 'btn' ,
'[class.btn-primary]' : 'variant() === "primary"' ,
'[class.btn-secondary]' : 'variant() === "secondary"' ,
'[class.btn-sm]' : 'size() === "small"' ,
'[class.btn-lg]' : 'size() === "large"' ,
'[class.disabled]' : 'disabled()' ,
'[attr.disabled]' : 'disabled() || null' ,
},
})
export class Button {
variant = input<'primary' | 'secondary' >('primary' );
size = input<'small' | 'medium' | 'large' >('medium' );
disabled = input (false , { transform : booleanAttribute });
}
Event Handling @Directive ({
selector : '[appClickOutside]' ,
host : {
'(document:click)' : 'onDocumentClick($event)' ,
},
})
export class ClickOutside {
private el = inject (ElementRef <HTMLElement >);
clickOutside = output<void >();
onDocumentClick (event : MouseEvent ) {
if (!this .el .nativeElement .contains (event.target as Node )) {
this .clickOutside .emit ();
}
}
}
Keyboard Shortcuts @Directive ({
selector : '[appShortcut]' ,
host : {
'(document:keydown)' : 'onKeydown($event)' ,
},
})
export class Shortcut {
key = input.required <string >({ alias : 'appShortcut' });
ctrl = input (false , { transform : booleanAttribute });
shift = input (false , { transform : booleanAttribute });
alt = input (false , { transform : booleanAttribute });
triggered = output<KeyboardEvent >();
onKeydown (event : KeyboardEvent ) {
const keyMatch = event.key .toLowerCase () === this .key ().toLowerCase ();
const ctrlMatch = this .ctrl () ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey ;
const shiftMatch = this .shift () ? event.shiftKey : !event.shiftKey ;
const altMatch = this .alt () ? event.altKey : !event.altKey ;
if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
event.preventDefault ();
this .triggered .emit (event);
}
}
}
Structural Directives Use structural directives for DOM manipulation beyond control flow (portals, overlays, dynamic insertion points). For conditionals and loops, use native @if, @for, @switch.
Portal Directive Render content in a different DOM location:
import { Directive , inject, TemplateRef , ViewContainerRef , OnInit , OnDestroy , input } from '@angular/core' ;
@Directive ({
selector : '[appPortal]' ,
})
export class Portal implements OnInit , OnDestroy {
private templateRef = inject (TemplateRef <any >);
private viewContainerRef = inject (ViewContainerRef );
private viewRef : EmbeddedViewRef <any > | null = null ;
target = input<string | HTMLElement >('body' , { alias : 'appPortal' });
ngOnInit ( ) {
const container = this .getContainer ();
if (container) {
this .viewRef = this .viewContainerRef .createEmbeddedView (this .templateRef );
this .viewRef .rootNodes .forEach (node => container.appendChild (node));
}
}
ngOnDestroy ( ) {
this .viewRef ?.destroy ();
}
private getContainer (): HTMLElement | null {
const target = this .target ();
if (typeof target === 'string' ) {
return document .querySelector (target);
}
return target;
}
}
Lazy Render Directive Defer rendering until condition is met (one-time):
@Directive ({
selector : '[appLazyRender]' ,
})
export class LazyRender {
private templateRef = inject (TemplateRef <any >);
private viewContainer = inject (ViewContainerRef );
private rendered = false ;
condition = input.required <boolean >({ alias : 'appLazyRender' });
constructor ( ) {
effect (() => {
if (this .condition () && !this .rendered ) {
this .viewContainer .createEmbeddedView (this .templateRef );
this .rendered = true ;
}
});
}
}
Template Outlet with Context interface TemplateContext <T> {
$implicit : T;
item : T;
index : number ;
}
@Directive ({
selector : '[appTemplateOutlet]' ,
})
export class TemplateOutlet <T> {
private viewContainer = inject (ViewContainerRef );
private currentView : EmbeddedViewRef <TemplateContext <T>> | null = null ;
template = input.required <TemplateRef <TemplateContext <T>>>({ alias : 'appTemplateOutlet' });
context = input.required <T>({ alias : 'appTemplateOutletContext' });
index = input (0 , { alias : 'appTemplateOutletIndex' });
constructor ( ) {
effect (() => {
const template = this .template ();
const context = this .context ();
const index = this .index ();
if (this .currentView ) {
this .currentView .context .$implicit = context;
this .currentView .context .item = context;
this .currentView .context .index = index;
this .currentView .markForCheck ();
} else {
this .currentView = this .viewContainer .createEmbeddedView (template, {
$implicit : context,
item : context,
index,
});
}
});
}
}
Host Directives Compose directives on components or other directives:
@Directive ({
selector : '[focusable]' ,
host : {
'tabindex' : '0' ,
'(focus)' : 'onFocus()' ,
'(blur)' : 'onBlur()' ,
'[class.focused]' : 'isFocused()' ,
},
})
export class Focusable {
isFocused = signal (false );
onFocus ( ) { this .isFocused .set (true ); }
onBlur ( ) { this .isFocused .set (false ); }
}
@Directive ({
selector : '[disableable]' ,
host : {
'[class.disabled]' : 'disabled()' ,
'[attr.aria-disabled]' : 'disabled()' ,
},
})
export class Disableable {
disabled = input (false , { transform : booleanAttribute });
}
@Component ({
selector : 'app-custom-button' ,
hostDirectives : [
Focusable ,
{
directive : Disableable ,
inputs : ['disabled' ],
},
],
host : {
'role' : 'button' ,
'(click)' : 'onClick($event)' ,
'(keydown.enter)' : 'onClick($event)' ,
'(keydown.space)' : 'onClick($event)' ,
},
template : `<ng-content />` ,
})
export class CustomButton {
private disableable = inject (Disableable );
clicked = output<void >();
onClick (event : Event ) {
if (!this .disableable .disabled ()) {
this .clicked .emit ();
}
}
}
Exposing Host Directive Outputs @Directive ({
selector : '[hoverable]' ,
host : {
'(mouseenter)' : 'onEnter()' ,
'(mouseleave)' : 'onLeave()' ,
'[class.hovered]' : 'isHovered()' ,
},
})
export class Hoverable {
isHovered = signal (false );
hoverChange = output<boolean >();
onEnter ( ) {
this .isHovered .set (true );
this .hoverChange .emit (true );
}
onLeave ( ) {
this .isHovered .set (false );
this .hoverChange .emit (false );
}
}
@Component ({
selector : 'app-card' ,
hostDirectives : [
{
directive : Hoverable ,
outputs : ['hoverChange' ],
},
],
template : `<ng-content />` ,
})
export class Card {}
Directive Composition API Combine multiple behaviors:
@Directive ({ selector : '[withRipple]' })
export class Ripple {
}
@Directive ({ selector : '[withElevation]' })
export class Elevation {
elevation = input (2 );
}
@Component ({
selector : 'app-material-button' ,
hostDirectives : [
Ripple ,
{
directive : Elevation ,
inputs : ['elevation' ],
},
{
directive : Disableable ,
inputs : ['disabled' ],
},
],
template : `<ng-content />` ,
})
export class MaterialButton {}