Angular 72 🅰️ Custom Directives in Depth
In the previous chapters, you encountered directives repeatedly — *ngIf, *ngFor, [ngClass], [ngStyle], [ngModel]. They are the tools that shape the DOM, attach behavior to elements, and make templates dynamic. But until now, every directive you used was one Angular shipped. This chapter is about writing your own.
Custom directives are the extension mechanism of Angular templates. When you find yourself repeating the same DOM manipulation — highlighting on hover, auto-focusing inputs, detecting clicks outside a dropdown, lazy-loading images — the correct answer is almost never “copy that logic into every component.” It is a directive. A directive encapsulates the behavior once, applies it everywhere with an attribute, and keeps component templates clean of imperative DOM code.
This chapter covers the two categories of custom directives: attribute directives (which change appearance or behavior of an element) and structural directives (which change the DOM structure itself). It also covers the directive composition API, the modern way to attach directives to a component’s host element without inheritance. The chapter assumes you have read the previous chapter on HostBinding and HostListener; directives are where those tools live.
Key point: A component is a directive with a template. An attribute directive changes how an existing element looks or behaves. A structural directive changes the DOM by adding, removing, or manipulating elements. Every directive is a class with a @Directive() decorator that contains a selector. The selector is what tells Angular which elements in a template should have the directive applied .
Why custom directives exist
Before writing one, it is worth understanding the problem they solve and why the solution is not a component.
The repetition problem. A hover highlight is three lines of logic: listen for mouseenter, set a style, listen for mouseleave, clear it. If you have six places in your application that need this behavior, a component is the wrong abstraction. A component renders something; a highlight directive attaches behavior to something that already exists. Repeating the logic six times is a maintenance trap. A directive writes it once and applies it with one attribute .
The encapsulation problem. DOM manipulation in a component’s TypeScript is invisible in the template. A reader looking at <p class="promo"> does not know that some constructor code is changing its background color on hover. An attribute directive makes the behavior visible: <p appHighlight class="promo"> announces that this element has hover highlighting .
The structural problem. Some behaviors need to change the DOM itself — conditionally show a block, repeat a template for each item in a list, render one of several possible layouts. Attribute directives cannot do this. They receive an element that already exists and modify it. Structural directives receive a template and decide whether and how many times to instantiate it .
The composition problem. A component may need behaviors that are conceptually separate: a menu that can be resized, dragged, and tooltipped. Building all of this into the component mixes concerns and bloats the class. The directive composition API lets a component declare hostDirectives: [Resizable, Draggable, Tooltip] and receive all three behaviors on its host element without inheritance and without the consumer knowing the directive selectors .
The trade-off. Directives add indirection. A directive’s behavior is not visible in the template that uses it — only its selector. Overusing directives for things that should be components makes templates cryptic. The rule of thumb: if it renders markup, it is a component. If it modifies existing markup, it is a directive.
a. Attribute Directives: Changing Appearance and Behavior
An attribute directive is a class with @Directive() and a selector that matches an attribute. Angular instantiates the directive for every element in the template that matches the selector. The directive receives a reference to that element through ElementRef and can modify it.
The canonical example is a highlight directive. The CLI generates the file:
ng generate directive highlight
The generated class has the selector [appHighlight] and an empty body. The first thing to add is ElementRef, which gives access to the host DOM element through its nativeElement property .
import { Directive, ElementRef } from '@angular/core';
@Directive({
standalone: true,
selector: '[appHighlight]',
})
export class HighlightDirective {
constructor(private el: ElementRef) {
this.el.nativeElement.style.backgroundColor = 'yellow';
}
}
Applying <p appHighlight>Highlight me!</p> creates an instance of the directive, injects the <p> element, and sets its background to yellow. The element does not need to know anything; the directive finds it by selector.
Modifying nativeElement.style directly works, but Angular recommends Renderer2 for DOM manipulation. Renderer2 abstracts the platform, so the code works in server-side rendering and in environments where the DOM is not directly accessible . For simple style changes in a browser-only application, ElementRef is acceptable. For anything more complex, Renderer2 is the correct tool.
The directive becomes useful when it accepts configuration. An @Input() aliased to the directive’s selector lets the consumer pass a value using the same attribute that applies the directive:
@Input() appHighlight = 'yellow';
Now <p [appHighlight]="'lightblue'"> applies the directive and sets the highlight color in one binding . The selector and the input share the same name, which is why the directive is usable with a value without a separate attribute.
Events complete the picture. @HostListener binds a method to an event on the host element:
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight('');
}
The directive now has state (appHighlight), behavior (highlight), and lifecycle (the event listeners). It is a self-contained piece of DOM logic that any element can use .
b. Structural Directives: Changing the DOM
A structural directive changes the DOM by adding, removing, or repeating elements. It does this by receiving a TemplateRef (the content it controls) and a ViewContainerRef (the location where it can render that content). The directive decides when to call viewContainer.createEmbeddedView(templateRef) and when to call viewContainer.clear() .
The asterisk syntax is the key to understanding structural directives. When you write *appUnless="condition", Angular transforms it into an <ng-template> that hosts the directive and wraps the element and its descendants:
<p *appUnless="condition">Content</p>
becomes:
<ng-template [appUnless]="condition">
<p>Content</p>
</ng-template>
The ng-template is the actual element the directive is applied to. The <p> is the template content that the directive may or may not render .
A custom *appUnless directive (the opposite of *ngIf) injects TemplateRef and ViewContainerRef, and uses a setter input to react to condition changes:
@Directive({
standalone: true,
selector: '[appUnless]',
})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
When the condition is falsy and no view exists, the directive creates the embedded view. When the condition is truthy and a view exists, it clears the container. The hasView flag prevents redundant work on every change detection cycle .
A structural directive can also provide a template context — data that the template can reference through let variables. The *ngFor directive provides index, first, last, even, and odd in addition to the item itself. A custom directive does this by passing an object as the second argument to createEmbeddedView:
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: item,
index: i,
first: i === 0,
last: i === collection.length - 1,
});
The $implicit property is what let item binds to when no explicit key is given. The other properties are bound with let i="index" and similar syntax .
One limitation: only one structural directive per element can use the shorthand syntax. Because the asterisk creates a single <ng-template>, applying two structural directives to the same element is ambiguous — which one wraps the other? Use <ng-container> as a wrapper when you need both, or use the long-form <ng-template> syntax explicitly .
c. Directive Composition and the hostDirectives API
The directive composition API lets a component apply directives to its own host element without the consumer knowing the directive selectors. It is defined with the hostDirectives property in the @Component or @Directive decorator .
@Component({
selector: 'admin-menu',
template: 'admin-menu.html',
hostDirectives: [MenuBehavior],
})
export class AdminMenu {}
When Angular renders <admin-menu>, it also creates an instance of MenuBehavior and applies its host bindings to the <admin-menu> element. The directive’s selector is ignored — MenuBehavior does not need a selector that matches admin-menu .
By default, the inputs and outputs of host directives are not exposed on the component. A consumer of <admin-menu> cannot bind to menuId or listen for menuClosed unless the component explicitly includes them:
@Component({
hostDirectives: [{
directive: MenuBehavior,
inputs: ['menuId'],
outputs: ['menuClosed'],
}],
})
export class AdminMenu {}
The inputs and outputs can also be aliased to avoid name collisions:
inputs: ['menuId: id'],
outputs: ['menuClosed: closed'],
Now the consumer writes <admin-menu id="top-menu" (closed)="logMenuClosed()">. The directive’s internals are hidden behind the component’s public API .
The execution order of host directives matters. Host directives always execute their constructor, lifecycle hooks, and bindings before the component or directive they are applied to. This means a component can override any host binding set by a host directive, and a deeper chain of host directives resolves outermost-first .
A practical caveat: inputs defined on a host directive are not automatically available in the component’s template. If you write hostDirectives: [{ directive: SomeDirective, inputs: ['someId'] }], the someId input is exposed to the component’s consumers, but the component’s own template cannot read it through this.someId. You need a separate @Input() on the component if the template needs the value .
Host directives must be standalone: true. They cannot be declared in an NgModule .
Complete Example Session
This session builds a hover-highlight attribute directive, a custom structural directive that mimics *ngIf, and a composed component using hostDirectives.
// ============================================
// PART 1: THE ATTRIBUTE DIRECTIVE
// ============================================
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
standalone: true,
selector: '[appHighlight]',
})
export class HighlightDirective {
@Input() appHighlight = 'yellow';
@Input() defaultColor = '';
constructor(private el: ElementRef) {}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.appHighlight);
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(this.defaultColor);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
// Usage:
// <p [appHighlight]="'lightblue'" [defaultColor]="'transparent'">
// Hover me
// </p>
// ============================================
// PART 2: THE CUSTOM STRUCTURAL DIRECTIVE
// ============================================
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
standalone: true,
selector: '[appUnless]',
})
export class UnlessDirective {
private hasView = false;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef
) {}
@Input() set appUnless(condition: boolean) {
if (!condition && !this.hasView) {
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (condition && this.hasView) {
this.viewContainer.clear();
this.hasView = false;
}
}
}
// Usage:
// <p *appUnless="isHidden">Shown when isHidden is false</p>
// <p *appUnless="!isHidden">Shown when isHidden is true</p>
// ============================================
// PART 3: THE STRUCTURAL DIRECTIVE WITH CONTEXT
// ============================================
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
standalone: true,
selector: '[appRepeat]',
})
export class RepeatDirective {
constructor(
private templateRef: TemplateRef<unknown>,
private viewContainer: ViewContainerRef
) {}
@Input() set appRepeat(collection: unknown[]) {
this.viewContainer.clear();
collection.forEach((item, index) => {
this.viewContainer.createEmbeddedView(this.templateRef, {
$implicit: item,
index,
first: index === 0,
last: index === collection.length - 1,
});
});
}
}
// Usage:
// <ul>
// <li *appRepeat="let item of items; index as i; first as isFirst">
// {{ i }}: {{ item }} {{ isFirst ? '(first)' : '' }}
// </li>
// </ul>
// ============================================
// PART 4: THE HOST DIRECTIVE
// ============================================
import { Directive, HostListener, input } from '@angular/core';
@Directive({
standalone: true,
selector: '[appClickOutside]',
})
export class ClickOutsideDirective {
enabled = input(true);
@HostListener('document:click', ['$event'])
onClick(event: MouseEvent) {
// In a real directive, check whether the click was
// inside the host element and emit an output if not.
}
}
// ============================================
// PART 5: THE COMPOSED COMPONENT
// ============================================
import { Component } from '@angular/core';
@Component({
selector: 'app-dropdown',
standalone: true,
template: `
<button (click)="open = !open">Toggle</button>
@if (open) {
<div class="menu"><ng-content /></div>
}
`,
hostDirectives: [
{
directive: ClickOutsideDirective,
inputs: ['enabled'],
},
],
})
export class DropdownComponent {
open = false;
}
// Usage:
// <app-dropdown [enabled]="true">
// <a href="/profile">Profile</a>
// </app-dropdown>
// ============================================
// PART 6: THE DIRECTIVE WITH RENDERER2
// ============================================
import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';
@Directive({
standalone: true,
selector: '[appBorder]',
})
export class BorderDirective {
@Input() appBorder = '2px solid black';
constructor(
private el: ElementRef,
private renderer: Renderer2
) {}
@HostListener('mouseenter') onEnter() {
this.renderer.setStyle(this.el.nativeElement, 'border', this.appBorder);
}
@HostListener('mouseleave') onLeave() {
this.renderer.removeStyle(this.el.nativeElement, 'border');
}
}
// ============================================
// PART 7: THE AUTO-FOCUS DIRECTIVE
// ============================================
import { Directive, ElementRef, AfterViewInit } from '@angular/core';
@Directive({
standalone: true,
selector: '[appAutoFocus]',
})
export class AutoFocusDirective implements AfterViewInit {
constructor(private el: ElementRef) {}
ngAfterViewInit() {
this.el.nativeElement.focus();
}
}
// Usage: <input appAutoFocus />
// ============================================
// PART 8: THE LAZY-LOAD DIRECTIVE
// ============================================
import { Directive, ElementRef, Input, OnDestroy } from '@angular/core';
@Directive({
standalone: true,
selector: 'img[appLazyLoad]',
})
export class LazyLoadDirective implements OnDestroy {
@Input() appLazyLoad = '';
private observer?: IntersectionObserver;
constructor(private el: ElementRef<HTMLImageElement>) {
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.el.nativeElement.src = this.appLazyLoad;
this.observer?.disconnect();
}
});
});
this.observer.observe(this.el.nativeElement);
}
ngOnDestroy() {
this.observer?.disconnect();
}
}
// Usage: <img [appLazyLoad]="imageUrl" alt="Photo" />
// ============================================
// PART 9: THE COPY-TO-CLIPBOARD DIRECTIVE
// ============================================
import { Directive, HostListener, input } from '@angular/core';
@Directive({
standalone: true,
selector: '[appCopyClipboard]',
})
export class CopyClipboardDirective {
textToCopy = input.required<string>({ alias: 'appCopyClipboard' });
@HostListener('click')
async copy() {
try {
await navigator.clipboard.writeText(this.textToCopy());
} catch (err) {
console.error('Copy failed', err);
}
}
}
// Usage: <button [appCopyClipboard]="'Hello'">Copy</button>
// ============================================
// PART 10: THE STANDALONE DIRECTIVE IMPORT
// ============================================
import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';
import { UnlessDirective } from './unless.directive';
@Component({
selector: 'app-demo',
standalone: true,
imports: [HighlightDirective, UnlessDirective],
template: `
<p [appHighlight]="'lightblue'">Hover me</p>
<p *appUnless="false">Shown</p>
`,
})
export class DemoComponent {}
The ten parts cover the attribute directive, the custom structural directive, the structural directive with context, a host directive, the composed component, Renderer2 usage, auto-focus, lazy-load, copy-to-clipboard, and standalone imports.
Quick Reference
The Two Types of Custom Directives
| Type | Purpose | Selector Example | Core Dependencies |
|---|---|---|---|
| Attribute | Change appearance/behavior | [appHighlight] | ElementRef, HostListener |
| Structural | Change DOM structure | [appUnless] | TemplateRef, ViewContainerRef |
The Directive Decorator Options
| Option | Purpose |
|---|---|
selector | CSS selector that identifies the directive |
standalone | Whether the directive is standalone (default: true in modern Angular) |
host | Host bindings and listeners (modern replacement for @HostBinding/@HostListener) |
hostDirectives | Directives to apply to the host element |
inputs / outputs | Explicit API surface for host directives |
The Structural Directive Pattern
| Step | Code |
|---|---|
| Inject dependencies | constructor(private tpl: TemplateRef, private vcr: ViewContainerRef) |
| Show content | this.vcr.createEmbeddedView(this.tpl) |
| Hide content | this.vcr.clear() |
| Provide context | this.vcr.createEmbeddedView(this.tpl, { $implicit: item }) |
The Shorthand Translation
| Shorthand | Long Form |
|---|---|
*appUnless="cond" | <ng-template [appUnless]="cond">...</ng-template> |
*appRepeat="let x of xs" | <ng-template appRepeat let-x [appRepeatOf]="xs">...</ng-template> |
*ngFor="let x of xs; index as i" | <ng-template ngFor let-x [ngForOf]="xs" let-i="index"> |
Best Practices
✅ Do This:
// Use standalone directives and import them
@Component({ imports: [HighlightDirective] }) // ✅
// Use Renderer2 for DOM manipulation
this.renderer.setStyle(this.el.nativeElement, 'border', '2px solid'); // ✅
// Use the host property in the directive decorator
@Directive({
host: { '(mouseenter)': 'onEnter()', '[class.active]': 'isActive' },
}) // ✅
// Expose host directive inputs/outputs explicitly
hostDirectives: [{ directive: MenuBehavior, inputs: ['menuId'] }] // ✅
// Use input() signal for directive configuration
color = input('yellow'); // ✅
❌ Don’t Do This:
// Don't use @HostBinding/@HostListener in new code
@HostBinding('style.color') color; // ❌ use host: {}
// Don't manipulate DOM directly without Renderer2 in complex cases
this.el.nativeElement.innerHTML = '<p>...</p>'; // ❌ security risk
// Don't use structural directives for simple conditions
// Use @if / @for block syntax instead.
// Don't apply two structural directives to the same element
// <p *ngIf="x" *ngFor="let y of ys"> // ❌ invalid
// Don't forget to import standalone directives
// They must be in the component's imports array. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Directive not applied | Missing import in component | Add to imports array |
| Input not binding | Selector and input name mismatch | Alias input to selector name |
| Structural directive doesn’t toggle | Missing hasView guard | Track whether the view exists |
| Host directive input not in template | Inputs exposed to consumer only | Add separate @Input() on component |
| DOM manipulation breaks SSR | Direct nativeElement access | Use Renderer2 |
| Multiple structural directives error | Only one <ng-template> per element | Use <ng-container> wrapper |
Real-World Examples
1. Hover Highlight
@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
@HostListener('mouseenter') onEnter() { /* set color */ }
}
2. Custom If (Unless)
@Input() set appUnless(condition: boolean) {
condition ? this.vcr.clear() : this.vcr.createEmbeddedView(this.tpl);
}
3. Auto-Focus
ngAfterViewInit() { this.el.nativeElement.focus(); }
4. Click Outside
@HostListener('document:click', ['$event'])
onClick(event: MouseEvent) { /* check target */ }
5. Lazy Load Image
new IntersectionObserver((entries) => { /* load when visible */ });
6. Copy to Clipboard
navigator.clipboard.writeText(this.textToCopy());
7. Repeat with Context
collection.forEach((item, i) => {
this.vcr.createEmbeddedView(this.tpl, { $implicit: item, index: i });
});
8. Host Directive Composition
hostDirectives: [{ directive: TooltipDirective, inputs: ['text'] }]
9. Renderer2 Border
this.renderer.setStyle(this.el.nativeElement, 'border', '2px solid');
10. Standalone Import
@Component({ imports: [HighlightDirective, UnlessDirective] })
Visual: Attribute vs Structural Directive
┌──────────────────────────────────────────────┐
│ ATTRIBUTE DIRECTIVE │
│ │
│ <p appHighlight>Text</p> │
│ │ │
│ ▼ │
│ Directive modifies <p> in place │
│ ├─ Changes style │
│ ├─ Listens to events │
│ └─ Element stays in DOM │
│ │
├──────────────────────────────────────────────┤
│ STRUCTURAL DIRECTIVE │
│ │
│ <p *appUnless="cond">Text</p> │
│ │ │
│ ▼ │
│ Becomes: │
│ <ng-template [appUnless]="cond"> │
│ <p>Text</p> │
│ </ng-template> │
│ │ │
│ ▼ │
│ Directive decides whether to render │
│ ├─ createEmbeddedView() → add │
│ └─ clear() → remove │
│ │
└──────────────────────────────────────────────┘
Visual: The Asterisk Shorthand
┌──────────────────────────────────────────────┐
│ ASTERISK SHORTHAND TRANSLATION │
│ │
│ Shorthand: │
│ <p *appUnless="condition">Content</p> │
│ │
│ Long form: │
│ <ng-template [appUnless]="condition"> │
│ <p>Content</p> │
│ </ng-template> │
│ │
│ The directive applies to <ng-template>. │
│ The <p> is the template content. │
│ The directive decides whether to render. │
│ │
│ Only ONE structural directive per element. │
│ Use <ng-container> for wrapping. │
│ │
└──────────────────────────────────────────────┘
Visual: Structural Directive with Context
┌──────────────────────────────────────────────┐
│ TEMPLATE CONTEXT │
│ │
│ Directive: │
│ vcr.createEmbeddedView(tpl, { │
│ $implicit: item, ← let item │
│ index: i, ← let i="index" │
│ first: i === 0, ← let f="first" │
│ }); │
│ │
│ Template: │
│ <li *appRepeat="let item of items; │
│ index as i; │
│ first as f"> │
│ {{ i }}: {{ item }} │
│ </li> │
│ │
└──────────────────────────────────────────────┘
Visual: Directive Composition
┌──────────────────────────────────────────────┐
│ HOST DIRECTIVES │
│ │
│ @Component({ │
│ hostDirectives: [ │
│ MenuBehavior, │
│ { directive: Tooltip, │
│ inputs: ['text'] }, │
│ ], │
│ }) │
│ class AdminMenu {} │
│ │
│ <admin-menu text="Settings"> │
│ │ │
│ ├─ MenuBehavior applied │
│ ├─ Tooltip applied │
│ └─ Component template rendered │
│ │
│ Consumer sees only <admin-menu>. │
│ Directive selectors are hidden. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Attribute directive | Changes appearance/behavior in place |
| Structural directive | Changes DOM structure |
| Structural dependencies | TemplateRef, ViewContainerRef |
| Asterisk shorthand | Creates <ng-template> wrapper |
| Template context | Object with $implicit and named keys |
| One structural directive | Per element with shorthand |
| Composition API | hostDirectives in decorator |
| Host directive order | Runs before the component |
| DOM manipulation | Prefer Renderer2 |
| Standalone | Default in modern Angular |
Key takeaways:
- A component is a directive with a template. Attribute directives modify existing elements. Structural directives decide whether and how many times to render a template. The selector determines which elements a directive applies to .
- Attribute directives use
ElementRefandHostListener. They receive the host element, can change its properties, styles, and classes, and can listen for events.Renderer2is preferred over directnativeElementaccess for portability . - Structural directives use
TemplateRefandViewContainerRef. The asterisk syntax wraps the element in an<ng-template>, which the directive receives. CallingcreateEmbeddedViewadds the content; callingclearremoves it . - The template context provides data to the template.
$implicitis the default binding forletvariables. Named keys are accessed withlet x="key". This is how*ngForexposesindex,first,last, andodd. - Only one structural directive can use the asterisk shorthand per element. The shorthand creates a single
<ng-template>, so two structural directives would be ambiguous. Use<ng-container>as a wrapper when nesting is needed . - The directive composition API attaches directives to a component’s host.
hostDirectivesapplies behavior without the consumer knowing the directive selectors. Inputs and outputs must be explicitly exposed to become part of the component’s API . - Host directives run before the component. Their constructors, lifecycle hooks, and host bindings execute first, which means the component can override their host bindings if needed. Chains of host directives resolve outermost-first .
Remember: Custom directives are how Angular applications extend the template language. An attribute directive is a reusable piece of DOM behavior — highlight on hover, focus on load, detect outside clicks. A structural directive is a reusable piece of DOM structure — conditionally show, repeat, or switch. The composition API is how components adopt those behaviors without inheritance. Write directives when the behavior is orthogonal to rendering. Write components when the behavior is the rendering. The boundary is clear, and respecting it keeps templates readable and components focused.
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!