typescript
55 lines · 7 steps
Building a tooltip directive in Angular
A standalone Angular directive that shows a template in a CDK overlay on hover and cleans up after itself.
Explained by
highlit
1import { Directive, ElementRef, HostListener, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
2import { Overlay, OverlayRef } from '@angular/cdk/overlay';
3import { TemplatePortal } from '@angular/cdk/portal';
4
5@Directive({
6 selector: '[appTooltip]',
7 standalone: true,
8})
9export class TooltipDirective implements OnDestroy {
10 @Input('appTooltip') content!: TemplateRef<unknown>;
11
12 private overlayRef?: OverlayRef;
13
14 constructor(
15 private readonly elementRef: ElementRef<HTMLElement>,
16 private readonly overlay: Overlay,
17 private readonly viewContainerRef: ViewContainerRef,
18 ) {}
19
20 @HostListener('mouseenter')
21 show(): void {
22 if (this.overlayRef?.hasAttached()) {
23 return;
24 }
25
26 const positionStrategy = this.overlay
27 .position()
28 .flexibleConnectedTo(this.elementRef)
29 .withPositions([
30 {
31 originX: 'center',
32 originY: 'top',
33 overlayX: 'center',
34 overlayY: 'bottom',
35 offsetY: -8,
36 },
37 ]);
38
39 this.overlayRef = this.overlay.create({
40 positionStrategy,
41 scrollStrategy: this.overlay.scrollStrategies.reposition(),
42 });
43
44 this.overlayRef.attach(new TemplatePortal(this.content, this.viewContainerRef));
45 }
46
47 @HostListener('mouseleave')
48 hide(): void {
49 this.overlayRef?.detach();
50 }
51
52 ngOnDestroy(): void {
53 this.overlayRef?.dispose();
54 }
55}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1The CDK overlay API lets you render content in a floating layer positioned relative to a host element.
- 2Guarding against an already-attached overlay avoids stacking duplicate tooltips on repeated hover events.
- 3Any imperatively created overlay must be disposed in ngOnDestroy to prevent leaking DOM and subscriptions.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
typescript
import { useEffect, useState } from "react"; interface Section { id: string;
Building a scroll-spy hook in React
custom-hooks
intersectionobserver
dom-observation
Intermediate
8 steps
typescript
import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; import { DataSource } from 'typeorm';
Per-tenant database connections in NestJS
multi-tenancy
connection-pooling
dependency-injection
Advanced
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-tooltip-directive-in-angular-explained-typescript-0ec2/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.