Key Takeaways
- A component is a directive with a template. Use it for a UI piece that owns its markup.
- An attribute directive packages reusable behavior for an existing element or component.
- Import a standalone directive into the component whose template uses it; generating its files isn't enough.
- Use host bindings for declarative styling, configurable inputs for reusable behavior, and a host-component test to verify the result.
Angular directives add behavior to existing elements. A component is a directive with a template: choose a component when you need a view of its own, and an attribute directive when the behavior belongs on an element that already exists.
Directive vs Component
Angular's three-category teaching model is useful, provided you remember that components are a kind of directive—not an unrelated alternative. The distinction is what each abstraction owns.
| Kind | What it owns or changes | Example |
|---|---|---|
| Component | A UI view with its own template | A profile card containing an avatar, name, and buttons |
| Attribute directive | The appearance or behavior of an existing host element | The appBigText directive below |
| Structural directive | Whether and how template content is instantiated | A custom permission-gated template |
Components
Use a component when you're defining markup as well as behavior. A profile card, navigation menu, or date picker needs its own template; adding an attribute to an existing element isn't the right abstraction for that view. For a companion example, see how to create an Angular component.
Attribute Directives
An attribute directive acts on a host element: the element to which you apply it. Directives can package substantial reusable behavior, not just styling; the deciding factor is whether they enhance an existing host rather than introduce their own view.
For one-off styling, prefer a CSS class or a template style binding. Create a directive when packaging that behavior for reuse actually removes duplication.
Structural Directives and Built-in Control Flow
Structural directives control the rendering of template content. Custom structural directives remain useful for reusable rendering policies; they commonly work with TemplateRef, which represents a template, and ViewContainerRef, which manages the views created from it.
For ordinary conditions and loops, use Angular's built-in @if and @for blocks. These are template syntax, not attribute directives, and don't require directive imports.
<!-- Template fragment: showUsers is a boolean; users have unique ids and names. -->
@if (showUsers) {
@for (user of users; track user.id) {
<p>{{ user.name }}</p>
}
}You'll still encounter *ngIf and *ngFor in existing applications. Both remain available in Angular 22, but NgIf and NgFor have been deprecated since Angular 20 in favor of the blocks. Don't treat their continued availability as a recommendation for new code.
Angular Directive Example
We'll keep the original big-text example: applying appBigText sets an element's font-size to 100px by default; it doesn't guarantee that the text or element is exactly 100 pixels tall. The updated implementation also accepts a numeric font size and responds when that input changes.
Generate the Project and Directive
Start with Angular CLI 22 installed and a Node.js runtime supported by Angular's version compatibility table. If you need the tooling setup first, follow the Angular CLI installation guide.
ng new angular-directives --standalone --style=css --defaults
cd angular-directives
ng generate directive big-text --standalone --selector=appBigText --type=directiveThe directive generator creates src/app/big-text.directive.ts and its companion big-text.directive.spec.ts. Here, --type=directive makes the filename suffix explicit, and --selector=appBigText fixes the attribute name used throughout the example.
Standalone generation is the default; the flag makes our intent explicit. This workflow doesn't require an app.module.ts update. Current CLI projects use concise root filenames such as app.ts and app.html, rather than the older app.component.ts and app.component.html convention.
Write the Custom Attribute Directive
Replace src/app/big-text.directive.ts with:
import { Directive, input } from '@angular/core';
@Directive({
selector: '[appBigText]',
standalone: true,
host: {
'[style.fontSize.px]': 'appBigTextSize()',
},
})
export class BigTextDirective {
readonly appBigTextSize = input(100);
}[appBigText]is an attribute selector: it matches elements carryingappBigText.input(100)declares a numeric input with a default value of 100. CallingappBigTextSize()reads its current value.- The host binding writes that value to the host's font size; the
.pxsuffix supplies the CSS unit.
When the input changes, Angular reevaluates the host binding and updates the element. No constructor-time DOM mutation or manual change handler is needed for this styling behavior.
This replaces the original assignment through ElementRef.nativeElement.style. Angular recommends the host metadata property over @HostBinding for new code, keeping the host's styles, attributes, and event bindings together.
Import the Directive into the Consuming Component
Replace src/app/app.ts with:
import { Component, signal } from '@angular/core';
import { BigTextDirective } from './big-text.directive';
@Component({
selector: 'app-root',
standalone: true,
imports: [BigTextDirective],
templateUrl: './app.html',
})
export class App {
readonly title = signal('angular-directives');
readonly fontSize = signal(48);
}There are two imports here with different jobs. The TypeScript import makes the class available in this file; imports: [BigTextDirective] makes it available to this component's template. Importing it into a parent component does not automatically make it available to a child's template.
Now replace src/app/app.html with:
<h1>Hello, {{ title() }}</h1>
<div appBigText>This text is huge.</div>
<p appBigText [appBigTextSize]="fontSize()">
This text uses a configurable size.
</p>
<button
type="button"
(click)="fontSize.set(fontSize() === 48 ? 72 : 48)"
>
Toggle font size
</button>The bare attribute uses the 100-pixel default. The paragraph starts at 48 pixels and toggles between 48 and 72. A signal is a reactive value: calling it reads the value, and set() changes it.
Run the Application
ng serveOpen the local address printed by the CLI; the development server's default port is 4200. Confirm that the first element stays large while the button changes only the configured paragraph.
Using the Directive in an NgModule Application
In an existing NgModule application, add this standalone directive to the relevant module's imports, not its declarations. Export it from a shared module if other modules need access. A non-standalone directive instead requires standalone: false and an NgModule declaration; don't mix those two registration models.
Verify the Directive with a Focused Test
The generated spec file is a starting point, not proof that template integration works. Test the directive through a small host component so you exercise selector matching, input binding, and the resulting style—not just class construction.
New Angular CLI projects use Vitest by default. Replace src/app/big-text.directive.spec.ts with this test:
import { Component, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { describe, expect, it } from 'vitest';
import { BigTextDirective } from './big-text.directive';
@Component({
standalone: true,
imports: [BigTextDirective],
template: `
<p appBigText>Default</p>
<p appBigText [appBigTextSize]="size()">Configured</p>
<p>Unchanged</p>
`,
})
class TestHost {
readonly size = signal(48);
}
describe('BigTextDirective', () => {
it('applies the default, updates its input, and leaves other elements alone', async () => {
TestBed.configureTestingModule({ imports: [TestHost] });
const fixture = TestBed.createComponent(TestHost);
await fixture.whenStable();
const paragraphs = (fixture.nativeElement as HTMLElement)
.querySelectorAll('p');
expect(paragraphs[0].style.fontSize).toBe('100px');
expect(paragraphs[1].style.fontSize).toBe('48px');
expect(paragraphs[2].style.fontSize).toBe('');
fixture.componentInstance.size.set(72);
await fixture.whenStable();
expect(paragraphs[1].style.fontSize).toBe('72px');
});
});Run only this spec with the CLI's include filter and disable watch mode for a single run:
ng test --include="**/big-text.directive.spec.ts" --watch=falseThis checks the default, a configured value, a later update, and an unaffected element. It's a practical example of the benefits of focused unit tests: protecting observable behavior while leaving the implementation free to change.
Common Mistakes
- Forgetting the template import. Without the directive in scope, a plain
appBigTextattribute can remain inert. A binding such as[appBigTextSize]produces an unknown-property error when Angular cannot resolve it to an element property or directive input. Check imports and spelling first. - Confusing selectors with inputs. This directive matches
appBigText, while its configurable input isappBigTextSize. Keep both names consistent; use an application or library prefix to avoid collisions. - Passing a string to a numeric input. Use
[appBigTextSize]="48", notappBigTextSize="48". This example declares a numeric input and doesn't define a string-to-number transform. - Reaching for DOM APIs unnecessarily. Prefer bindings for styles. Use native DOM access for genuine browser operations such as focus or measurement, and consider rendering timing. Renderer2 supports imperative rendering operations, but it isn't a security boundary or a blanket server-rendering fix.
FAQ
What is an Angular directive?
An Angular directive adds behavior to elements or components. Attribute directives enhance an existing host, structural directives control template rendering, and components are directives with their own templates.
What is the difference between an Angular directive and a component?
A component is a directive with a template. Use a component for a UI piece with its own markup, and an attribute directive for reusable behavior attached to an existing element.
How do I create and use a standalone Angular directive?
Generate it with ng generate directive, define its selector and behavior, and add it to the consuming standalone component's imports array. Then apply the selector in that component's template.
Are *ngIf and *ngFor still supported?
They remain available in Angular 22, but both have been deprecated since Angular 20. Use @if and @for for new conditional and repeated template content; custom structural directives remain useful for reusable rendering behavior.
Should an Angular directive use ElementRef or Renderer2 to change a style?
For a host style, prefer a declarative host binding rather than either API. ElementRef exposes the underlying element, while Renderer2 provides imperative rendering methods; neither is needed for the font-size example.
Further Reading
- Angular directives overview — the directive taxonomy and when to use each abstraction.
- Building attribute directives — selectors, reusable behavior, inputs, and host events.
- Angular CLI directive generator — standalone generation, selectors, filenames, and test options.
- Binding to host elements — styles, attributes, events, and the preferred host metadata syntax.
- Signal-based inputs — defaults, types, and input transforms.
- Built-in template control flow — conditions, loops, and tracking item identity.
- Testing attribute directives — host-component tests and assertions about observable behavior.
- Using DOM APIs in Angular — when imperative access is justified and what rendering caveats matter.
