Angular 10 🅰️ Attribute Directives
An attribute directive changes the appearance or behavior of an existing element without changing its structure. Where a structural directive decides whether an element exists, an attribute directive says how it looks or behaves while it exists. You’ve already used attribute directives — [class.active] and [style.color] are built-in attribute bindings. Angular also ships a few named ones (ngClass, ngStyle), and you can write your own. This chapter covers what they are, the built-ins you’ll meet, and how to build a simple custom one.
Key point: Attribute directives do not add or remove elements. They attach to an element that’s already there and modify it. The bracket syntax [directiveName]="expr" is the tell — no asterisk, no <ng-template> expansion, just a decorator on the element that does something with the value you give it.
What an attribute directive is
An attribute directive is a class with the @Directive decorator. It has a selector — usually an attribute selector in square brackets — and when Angular sees that attribute on an element, it instantiates the directive and attaches it to that element.
import { Directive, ElementRef, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
private el = inject(ElementRef<HTMLElement>);
constructor() {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}
Use it:
<p appHighlight>This paragraph is highlighted.</p>
Angular sees appHighlight on the <p>, creates a HighlightDirective, injects the <p> element, and the directive sets its background color. The element still exists — it just looks different.
Attribute vs structural:
| Attribute | Structural | |
|---|---|---|
| Syntax | [dir]="expr" or dir | *dir="expr" |
| Changes | Appearance / behavior | Adds / removes elements |
| Expands to | Nothing — attaches directly | <ng-template> wrapper |
| Examples | [class.active], ngClass, custom | *ngIf, *ngFor, custom |
Attribute vs component: A component is also a directive — but with a template. An attribute directive has no template. It just modifies the host element.
Why the distinction matters: Structural directives are always about presence. Attribute directives are about modification. When you’re deciding what to build, ask: does this thing exist or not, or does this thing look or behave differently? If the second, it’s an attribute directive.
The selector — attribute form
Attribute directives use attribute selectors. The bracket [...] is the syntax.
@Directive({ selector: '[appHighlight]' })
Matches any element with the attribute appHighlight:
<p appHighlight>...</p>
You can also bind a value to it:
<p [appHighlight]="'red'">...</p>
That requires an @Input with a matching name.
Other selector forms:
| Selector | Matches |
|---|---|
'[appHighlight]' | Attribute appHighlight |
'[appHighlight="on"]' | Attribute with specific value |
'appHighlight' | Element named appHighlight (rare) |
'.highlight' | Class highlight (rare) |
'[appHighlight], [appGlow]' | Either attribute |
Convention: prefix custom directives with app (the project’s prefix) to avoid colliding with native attributes.
Why attribute selectors: They’re the natural fit for “modify an existing element.” You don’t need an element named
app-highlight— you just tag any element with the attribute.
Injecting the host element — ElementRef
To modify the element, the directive needs a reference to it. ElementRef is the standard way.
import { Directive, ElementRef, inject, OnInit } from '@angular/core';
@Directive({
selector: '[appUnderline]',
standalone: true
})
export class UnderlineDirective implements OnInit {
private el = inject(ElementRef<HTMLElement>);
ngOnInit(): void {
this.el.nativeElement.style.textDecoration = 'underline';
}
}
ElementRef.nativeElement is the underlying DOM node. inject() is the modern way to get it; constructor injection (constructor(private el: ElementRef)) still works.
Type safety: ElementRef<HTMLElement> narrows nativeElement to HTMLElement instead of the default any. Always type it.
Direct DOM access is a last resort. Angular’s declarative bindings — [class], [style], [attr] — should handle most styling. Use ElementRef when you genuinely need imperative DOM access: measuring sizes, scrolling, focusing, integrating with a non-Angular library.
Why
ElementRefand notdocument.querySelector: The directive may be applied to multiple elements.ElementRefgives you this host element, not whichever one a global query happens to find first. It also stays correct when the component tree changes.
Reacting to @Input values
A directive without an input does one thing forever. With an input, it responds to the value you bind.
import { Directive, ElementRef, Input, inject, OnChanges, SimpleChanges } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective implements OnChanges {
@Input() appHighlight = 'yellow';
private el = inject(ElementRef<HTMLElement>);
ngOnChanges(_: SimpleChanges): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
}
Use it:
<p [appHighlight]="'pink'">Pink background.</p>
<p [appHighlight]="dynamicColor">Follows the variable.</p>
The input name must match the selector — appHighlight — for the value binding to reach the directive. That’s a convention, not a requirement, but Angular’s tooling and the built-in ngClass/ngStyle follow it.
Why ngOnChanges and not the constructor: Inputs are set after the constructor runs. ngOnChanges fires whenever an input changes, including the first time. That’s where reacting to input values belongs.
Alias form: If you want a shorter input name than the selector:
@Directive({ selector: '[appHighlight]', standalone: true })
export class HighlightDirective {
@Input('appHighlight') color = 'yellow';
// ...
}
Now [appHighlight]="'pink'" sets color.
Why
ngOnChangesmatters: A directive that reads its input in the constructor sees the initial value only. If the parent changes the bound expression later, onlyngOnChangesfires. For any directive that responds to a changing input,ngOnChangesis the hook.
Handling events — HostListener
A directive can listen to events on its host element.
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
private el = inject(ElementRef<HTMLElement>);
@HostListener('mouseenter')
onEnter(): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave')
onLeave(): void {
this.el.nativeElement.style.backgroundColor = '';
}
}
The directive highlights on hover and removes the highlight when the mouse leaves.
@HostListener arguments: @HostListener('event', ['$event', ...args]) — pass event properties or the event object to the handler.
@HostListener('keydown', ['$event'])
onKey(event: KeyboardEvent): void {
console.log(event.key);
}
Modern alternative: The host object on the decorator:
@Directive({
selector: '[appHighlight]',
standalone: true,
host: {
'(mouseenter)': 'onEnter()',
'(mouseleave)': 'onLeave()',
'[class.active]': 'isActive',
'[attr.role]': '"button"'
}
})
export class HighlightDirective {
isActive = false;
onEnter(): void { this.isActive = true; }
onLeave(): void { this.isActive = false; }
}
host puts bindings and listeners in one place, declaratively. No ElementRef needed if you can express the change via bindings — [class.active] is cheaper and safer than mutating style.
Why
hostis preferred: It works with Angular’s binding system rather than bypassing it.[class.active]and[attr.role]are optimized, and they keep the directive declarative. Reach forElementRefonly when you need something bindings can’t express — scroll positions, focus, measurement.
@HostBinding — binding host properties
@HostBinding binds a directive property to a host element property, attribute, class, or style.
import { Directive, HostBinding, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
@HostBinding('style.backgroundColor') bg = '';
ngOnInit(): void {
this.bg = this.appHighlight;
}
}
@HostBinding('style.backgroundColor') keeps bg in sync with the element’s backgroundColor style.
Common @HostBinding targets:
| Target | Example |
|---|---|
class.active | Toggle a class |
style.color | Bind a style |
attr.role | Bind an attribute |
disabled | Bind a property |
title | Bind a property |
Modern alternative: The host object:
@Directive({
selector: '[appHighlight]',
standalone: true,
host: {
'[class.active]': 'isActive',
'[style.backgroundColor]': 'bg',
'[attr.role]': '"button"'
}
})
export class HighlightDirective {
isActive = false;
bg = 'yellow';
}
Same result, no decorators. Angular’s team recommends the host object for new code — it’s more discoverable and centralizes everything about the host.
Why
hostover@HostBinding: Thehostobject is easier to see. A reader doesn’t have to scan the class for@HostBinding/@HostListenerdecorators — everything the directive does to its host is listed in the decorator. It also plays better with type-checking.
The built-in directives — ngClass and ngStyle
Angular ships two attribute directives you’ll see in older code: ngClass and ngStyle. The bracket bindings [class] and [style] have largely replaced them, but they still work.
ngClass:
<div [ngClass]="{ active: isActive, error: hasError }">...</div>
<div [ngClass]="['card', size]">...</div>
<div [ngClass]="'single-class'">...</div>
ngStyle:
<div [ngStyle]="{ color: textColor, 'font-size': fontSize + 'px' }">...</div>
Direct equivalents with bindings:
<div [class]="{ active: isActive, error: hasError }">...</div>
<div [class]="['card', size]">...</div>
<div [style]="{ color: textColor, 'font-size': fontSize + 'px' }">...</div>
The bracket forms are newer and don’t require CommonModule. Use them.
When ngClass is still needed: Rarely. The main case is combining multiple class sources where you want merge semantics — [class] replaces the class list, ngClass merges. If you need to add classes without removing existing ones, ngClass may be necessary. But if you control the markup, use [class.foo] for single toggles and [class]="expr" for the whole list.
Why both exist:
ngClassandngStylecame first.[class]and[style]bindings were added later and do the same job without a directive. New code should prefer bindings — fewer imports, better type-checking.
Attribute directives vs components
Both are directives. The difference:
| Attribute directive | Component | |
|---|---|---|
| Has a template | ❌ | ✅ |
| Selector | Usually [attr] | Usually app-name |
| Purpose | Modify an element | Render a view |
| Example | appHighlight | <app-card> |
If your class has a template or templateUrl in the decorator, it’s a component. Otherwise it’s a directive. Both use the same DI, lifecycle, and inputs.
Why the framework splits them: Components render their own view. Attribute directives modify a host they don’t own. Keeping them separate keeps the mental model clean — “a thing I render” vs “a thing I attach to something else.”
A full example
A directive that highlights on hover and accepts a color input.
The directive:
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
private el = inject(ElementRef<HTMLElement>);
@HostListener('mouseenter')
onEnter(): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave')
onLeave(): void {
this.el.nativeElement.style.backgroundColor = '';
}
}
Using it:
import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
@Component({
selector: 'app-demo',
standalone: true,
imports: [HighlightDirective],
template: `
<p [appHighlight]="'pink'">Hover me — pink.</p>
<p [appHighlight]="color">Hover me — {{ color }}.</p>
<p appHighlight>Default yellow.</p>
`
})
export class DemoComponent {
color = 'lightblue';
}
Three paragraphs. The first uses a literal color, the second a bound variable, the third the default. Each highlights on hover.
Why it’s clean: The directive is one class, and it’s declared in imports like a component. No CommonModule, no module declaration. That’s what standalone directives give you.
Why this shape: A directive should have a narrow job. This one: highlight on hover with a configurable color. If it also logged events, tracked analytics, and modified text, it’d be doing too much. Small, focused directives compose well.
Complete Example Session
# ============================================
# PART 1: GENERATE A DIRECTIVE
# ============================================
ng generate directive highlight
# [ CREATE src/app/highlight.directive.ts ]
# [ CREATE src/app/highlight.directive.spec.ts ]
# ============================================
# PART 2: WRITE THE DIRECTIVE
# ============================================
cat > src/app/highlight.directive.ts << 'EOF'
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core';
@Directive({
selector: '[appHighlight]',
standalone: true
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
private el = inject(ElementRef<HTMLElement>);
@HostListener('mouseenter')
onEnter(): void {
this.el.nativeElement.style.backgroundColor = this.appHighlight;
}
@HostListener('mouseleave')
onLeave(): void {
this.el.nativeElement.style.backgroundColor = '';
}
}
EOF
# ============================================
# PART 3: USE IT
# ============================================
cat > src/app/demo/demo.component.ts << 'EOF'
import { Component } from '@angular/core';
import { HighlightDirective } from '../highlight.directive';
@Component({
selector: 'app-demo',
standalone: true,
imports: [HighlightDirective],
template: `
<p [appHighlight]="'pink'">Hover me — pink.</p>
<p [appHighlight]="color">Hover me — {{ color }}.</p>
<p appHighlight>Default yellow.</p>
`
})
export class DemoComponent {
color = 'lightblue';
}
EOF
# ============================================
# PART 4: HOST OBJECT VARIANT
# ============================================
cat > src/app/glow/glow.directive.ts << 'EOF'
import { Directive, Input } from '@angular/core';
@Directive({
selector: '[appGlow]',
standalone: true,
host: {
'[class.glowing]': 'isGlowing',
'(mouseenter)': 'isGlowing = true',
'(mouseleave)': 'isGlowing = false'
}
})
export class GlowDirective {
@Input() color = 'cyan';
isGlowing = false;
}
EOF
# ============================================
# PART 5: STYLE IT
# ============================================
cat > src/app/glow/glow.directive.css << 'EOF'
:host.glowing {
box-shadow: 0 0 10px currentColor;
color: var(--glow-color, cyan);
}
EOF
# ============================================
# PART 6: SERVE
# ============================================
ng serve
# [ Local: http://localhost:4200/ ]
Quick Reference
Directive Anatomy
| Part | Purpose |
|---|---|
@Directive decorator | Marks the class as a directive |
selector | Where it applies |
standalone: true | No NgModule needed |
host | Bindings and listeners on host |
| Class body | Logic and state |
Selector Forms
| Selector | Matches |
|---|---|
'[appHighlight]' | Attribute |
'[appHighlight="on"]' | Attribute with value |
'appHighlight' | Element |
'.highlight' | Class |
'[a], [b]' | Either |
Host Element Access
| Tool | Purpose |
|---|---|
ElementRef | Raw DOM node |
@HostBinding | Bind host prop |
@HostListener | Listen on host |
host: {} | Declarative host config |
Input Naming
| Pattern | Meaning |
|---|---|
@Input() appHighlight | Input named appHighlight |
@Input('appHighlight') color | Alias to a different property |
Built-in Attribute Directives
| Directive | Purpose | Modern replacement |
|---|---|---|
ngClass | Class list | [class] / [class.foo] |
ngStyle | Style object | [style] / [style.foo] |
Decorator vs Host Object
| Concern | Decorator | Host object |
|---|---|---|
| Bind a class | @HostBinding('class.x') | '[class.x]': 'prop' |
| Bind a style | @HostBinding('style.x') | '[style.x]': 'prop' |
| Listen | @HostListener('event') | '(event)': 'handler()' |
| Recommended | Legacy | ✅ Modern |
CLI
| Command | Purpose |
|---|---|
ng g d NAME | Generate directive |
ng g d NAME --skip-tests | No spec |
ng g d NAME --flat | No folder |
Best Practices
✅ Do This:
// Use standalone directives
@Directive({ selector: '[appX]', standalone: true }) // ✅
// Prefix selectors to avoid collisions
selector: '[appHighlight]' // ✅
// Use the host object for bindings
host: { '[class.active]': 'isActive' } // ✅
// Type ElementRef
inject(ElementRef<HTMLElement>) // ✅
// React to input changes with ngOnChanges or signals
@Input() color = 'yellow'; // ✅
// Keep directives focused
// One job per directive // ✅
// Declare directives in imports of the component that uses them
imports: [HighlightDirective] // ✅
❌ Don’t Do This:
// Don't reach for DOM when bindings suffice
el.nativeElement.classList.add('active'); // ⚠️ use [class.active]
// Don't use ngClass/ngStyle for new code
[ngClass]="{ active: flag }" // ⚠️ use [class] form
// Don't name the selector without a prefix
selector: '[highlight]' // ⚠️ collides with HTML
// Don't skip standalone in new code
@Directive({ selector: '[appX]' }) // ⚠️ needs NgModule
// Don't do heavy work in the constructor
constructor() { this.computeLayout(); } // ⚠️ use ngOnInit
// Don't log in ngOnChanges
ngOnChanges() { console.log('changed'); } // ⚠️ runs every CD
// Don't forget to declare directives in imports
imports: [] // ❌ template error
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
| Forgetting to import | Directive not found | Add to imports |
| Selector without prefix | Name collision | Use app- prefix |
| Input name mismatch | Value binding does nothing | Match input to selector name |
| Reading input in constructor | Undefined | Use ngOnChanges |
| Heavy DOM work | Slow render | Use bindings where possible |
Not typing ElementRef | any everywhere | ElementRef<HTMLElement> |
Using ngClass for one class | Dead import | Use [class.foo] |
Mixing decorators and host | Confusing | Pick one — prefer host |
Real-World Examples
1. Highlight on hover
@Directive({ selector: '[appHighlight]', standalone: true })
export class HighlightDirective {
@HostListener('mouseenter') onEnter(): void { /* ... */ }
}
2. Toggle a class
@Directive({
selector: '[appActive]',
standalone: true,
host: { '[class.active]': 'active' }
})
export class ActiveDirective {
@Input() active = false;
}
3. Set an attribute
host: { '[attr.aria-busy]': 'loading' }
4. Bind to a style
host: { '[style.borderColor]': 'color' }
5. React to an input change
@Input() appHighlight = 'yellow';
ngOnChanges(): void { /* update */ }
6. Focus an element on init
ngOnInit(): void {
this.el.nativeElement.focus();
}
7. Listen for a specific key
@HostListener('keydown.enter', ['$event'])
onEnter(e: KeyboardEvent): void { /* ... */ }
8. Prevent default on click
@HostListener('click', ['$event'])
onClick(e: MouseEvent): void {
e.preventDefault();
}
9. Chain several attribute directives
<div appHighlight appGlow [appTrack]="'cta'">...</div>
10. Use ngClass for merge semantics
<div class="base" [ngClass]="{ active: isActive }">...</div>
11. Prefer [class] for new code
<div [class]="{ base: true, active: isActive }">...</div>
12. Alias an input
@Input('appHighlight') color = 'yellow';
13. Attribute directive without input
@Directive({ selector: '[appUnderline]', standalone: true })
export class UnderlineDirective implements OnInit {
ngOnInit(): void { /* set style once */ }
}
14. Conditional directive application
<!-- Apply the directive only when the flag is true -->
<div [appHighlight]="flag ? 'yellow' : ''">...</div>
15. Generate a directive
ng generate directive highlight
Visual: Attribute vs Structural
┌──────────────────────────────────────────────┐
│ Attribute Directive │
│ │
│ <p appHighlight>Hello</p> │
│ │
│ → The <p> still exists │
│ → The directive modifies it │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Structural Directive │
│ │
│ <p *ngIf="cond">Hello</p> │
│ │
│ → The <p> exists only when cond is truthy │
│ → Wrapped in <ng-template> │
│ │
└──────────────────────────────────────────────┘
Visual: Directive Lifecycle
┌──────────────────────────────────────────────┐
│ Angular matches selector │
│ │ │
│ ▼ │
│ Directive instantiated │
│ │ │
│ ▼ │
│ Constructor — DI available │
│ │ │
│ ▼ │
│ ngOnChanges — inputs set (first time) │
│ │ │
│ ▼ │
│ ngOnInit — setup │
│ │ │
│ ▼ │
│ (input changes → ngOnChanges) │
│ (host events → @HostListener handlers) │
│ │ │
│ ▼ │
│ ngOnDestroy — cleanup │
│ │
└──────────────────────────────────────────────┘
Visual: Decorators vs Host Object
┌──────────────────────────────────────────────┐
│ Legacy — decorators │
│ │
│ @HostBinding('class.active') isActive = false│
│ @HostListener('click') onClick() { } │
│ │
│ → Scattered through the class │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Modern — host object │
│ │
│ host: { │
│ '[class.active]': 'isActive', │
│ '(click)': 'onClick()' │
│ } │
│ │
│ → All in one place, visible in decorator │
│ │
└──────────────────────────────────────────────┘
Visual: Selector Precedence
┌──────────────────────────────────────────────┐
│ Selector forms and what they match │
│ │
│ '[appHighlight]' │
│ → <p appHighlight> │
│ │
│ '[appHighlight="on"]' │
│ → <p appHighlight="on"> │
│ │
│ 'appHighlight' │
│ → <appHighlight> │
│ │
│ '[a], [b]' │
│ → <p a> or <p b> │
│ │
└──────────────────────────────────────────────┘
Visual: Class vs Style Bindings
┌──────────────────────────────────────────────┐
│ Single class │
│ │
│ [class.active]="flag" │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Multiple classes │
│ │
│ [class]="{ active: a, error: b }" │
│ [class]="['card', size]" │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Single style with unit │
│ │
│ [style.width.px]="w" │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Multiple styles │
│ │
│ [style]="{ color: c, 'font-size': s }" │
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
| Attribute directive | Modifies an existing element |
| Structural directive | Adds or removes elements |
@Directive | Decorator for directive classes |
| Selector | Usually an attribute name |
ElementRef | Reference to the host DOM node |
@Input | Accept a value from the template |
@HostListener | Listen for events on the host |
@HostBinding | Bind a host property |
host: {} | Modern declarative host config |
ngClass / ngStyle | Legacy class/style directives |
[class.foo] / [style.foo] | Modern class/style bindings |
Key takeaways:
- Attribute directives modify an element that already exists — they don’t add or remove it
- They use attribute selectors (
[appHighlight]) and are declared inimportslike components ElementRefgives the directive a reference to its host element — use it sparingly@Inputlets the template pass a value;ngOnChangesreacts to changes@HostListenerlistens for events;@HostBindingbinds host properties- The
hostobject is the modern replacement for both — put bindings and listeners in the decorator ngClassandngStylestill work, but[class]and[style]bindings are preferred for new code- Use
[class.foo]for single toggles and[class]for full class lists - Prefix selectors with
appto avoid collisions with native attributes - Keep directives focused — one job each
Remember: Attribute directives are how you attach behavior and appearance to elements without changing whether they exist. ElementRef for the host, @Input for data, @HostListener/@HostBinding (or the host object) for host interaction. Reach for bindings first, ElementRef only when you need imperative DOM access. Keep them small, prefix the selector, and declare them in imports. That’s the whole craft.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!