| |

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:

AttributeStructural
Syntax[dir]="expr" or dir*dir="expr"
ChangesAppearance / behaviorAdds / removes elements
Expands toNothing — 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:

SelectorMatches
'[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 ElementRef and not document.querySelector: The directive may be applied to multiple elements. ElementRef gives 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 ngOnChanges matters: A directive that reads its input in the constructor sees the initial value only. If the parent changes the bound expression later, only ngOnChanges fires. For any directive that responds to a changing input, ngOnChanges is 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 host is 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 for ElementRef only 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:

TargetExample
class.activeToggle a class
style.colorBind a style
attr.roleBind an attribute
disabledBind a property
titleBind 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 host over @HostBinding: The host object is easier to see. A reader doesn’t have to scan the class for @HostBinding/@HostListener decorators — 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: ngClass and ngStyle came 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 directiveComponent
Has a template
SelectorUsually [attr]Usually app-name
PurposeModify an elementRender a view
ExampleappHighlight<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

PartPurpose
@Directive decoratorMarks the class as a directive
selectorWhere it applies
standalone: trueNo NgModule needed
hostBindings and listeners on host
Class bodyLogic and state

Selector Forms

SelectorMatches
'[appHighlight]'Attribute
'[appHighlight="on"]'Attribute with value
'appHighlight'Element
'.highlight'Class
'[a], [b]'Either

Host Element Access

ToolPurpose
ElementRefRaw DOM node
@HostBindingBind host prop
@HostListenerListen on host
host: {}Declarative host config

Input Naming

PatternMeaning
@Input() appHighlightInput named appHighlight
@Input('appHighlight') colorAlias to a different property

Built-in Attribute Directives

DirectivePurposeModern replacement
ngClassClass list[class] / [class.foo]
ngStyleStyle object[style] / [style.foo]

Decorator vs Host Object

ConcernDecoratorHost object
Bind a class@HostBinding('class.x')'[class.x]': 'prop'
Bind a style@HostBinding('style.x')'[style.x]': 'prop'
Listen@HostListener('event')'(event)': 'handler()'
RecommendedLegacy✅ Modern

CLI

CommandPurpose
ng g d NAMEGenerate directive
ng g d NAME --skip-testsNo spec
ng g d NAME --flatNo 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

PitfallProblemSolution
Forgetting to importDirective not foundAdd to imports
Selector without prefixName collisionUse app- prefix
Input name mismatchValue binding does nothingMatch input to selector name
Reading input in constructorUndefinedUse ngOnChanges
Heavy DOM workSlow renderUse bindings where possible
Not typing ElementRefany everywhereElementRef<HTMLElement>
Using ngClass for one classDead importUse [class.foo]
Mixing decorators and hostConfusingPick 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

ConceptMeaning
Attribute directiveModifies an existing element
Structural directiveAdds or removes elements
@DirectiveDecorator for directive classes
SelectorUsually an attribute name
ElementRefReference to the host DOM node
@InputAccept a value from the template
@HostListenerListen for events on the host
@HostBindingBind a host property
host: {}Modern declarative host config
ngClass / ngStyleLegacy 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 in imports like components
  • ElementRef gives the directive a reference to its host element — use it sparingly
  • @Input lets the template pass a value; ngOnChanges reacts to changes
  • @HostListener listens for events; @HostBinding binds host properties
  • The host object is the modern replacement for both — put bindings and listeners in the decorator
  • ngClass and ngStyle still 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 app to 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!