Demo
<button appTooltip="Hello from string">Tooltip as string</button>
<svg [appTooltip]="templateForTooltip"> <!-- ... --></svg><ng-template #templateForTooltip> <strong>Hello</strong> from template </ng-template>
Angular CDK setup
The first step is to add a library. Write the command below to add @angular/cdk to your project.
ng add @angular/cdk
The second step is to add overlay styles to the global stylesheet file (styles.scss).
@import "~@angular/cdk/overlay-prebuilt.css";
This file is very small. This weight is around 1.2 kB
.
From @angular/cdk@15
OverlayModule
is not required to import anymore because Overlay
is provided into root
.
Creating directive
The directive is responsible for creating a component above (or below if above is no place) an element. As you can see, a tooltip is created on focus or moveenter events. After blur or moveleave the tooltip is destroyed.
@Directive({ selector: "[appTooltip]", standalone: true,})export class TooltipDirective implements OnDestroy { private element = inject<ElementRef<HTMLElement>>(ElementRef); private overlay = inject(Overlay); private viewContainer = inject(ViewContainerRef);
@Input() appTooltip!: TooltipData;
private overlayRef: OverlayRef | null = null;
@HostListener("mouseenter") @HostListener("focus") showTooltip(): void { if (this.overlayRef?.hasAttached() === true) { return; }
this.attachTooltip(); }
@HostListener("mouseleave") @HostListener("blur") hideTooltip(): void { if (this.overlayRef?.hasAttached() === true) { this.overlayRef?.detach(); } }
ngOnDestroy(): void { this.overlayRef?.dispose(); }
private attachTooltip(): void { if (this.overlayRef === null) { const positionStrategy = this.getPositionStrategy(); this.overlayRef = this.overlay.create({ positionStrategy }); }
const injector = Injector.create({ providers: [ { provide: TOOLTIP_DATA, useValue: this.appTooltip, }, ], }); const component = new ComponentPortal(TooltipContainerComponent, this.viewContainer, injector); this.overlayRef.attach(component); }
private getPositionStrategy(): PositionStrategy { return this.overlay .position() .flexibleConnectedTo(this.element) .withPositions([ { originX: "center", originY: "top", overlayX: "center", overlayY: "bottom", panelClass: "top", }, { originX: "center", originY: "bottom", overlayX: "center", overlayY: "top", panelClass: "bottom", }, ]); }}
Take a look at passing data between the component and the directive. To do it, I used Injector with a custom provider.
Look at getPositionStrategy
function
- in
flexibleConnectedTo
I defined a position connection between the directive and the tooltip, - In
withPositions
I defined available positions. If the first position does not fit on a screen, then use the second one
Tooltip container component
@Component({ selector: "app-tooltip-container", templateUrl: "./tooltip-container.component.html", styleUrls: ["./tooltip-container.component.scss"], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, imports: [NgTemplateOutlet],})export class TooltipContainerComponent { tooltipData = inject<TooltipData>(TOOLTIP_DATA);
isString(value: TooltipData): value is string { return typeof value === "string"; }
isTemplate(value: TooltipData): value is TemplateRef<void> { return this.tooltipData instanceof TemplateRef; }}
@let data = tooltipData;
@if (isString(data)) { {{ data }}} @else { <ng-template [ngTemplateOutlet]="data" />}
Take a look at asString and asTemplate getters. In HTML is not possible to cast a type, so I used a pattern with *ngIf
.
Adding styles for tooltip
:host { display: block; max-width: 12rem; padding: 0.7rem; font-size: 0.85rem; color: #fff; background: #000; border-radius: 0.25rem; box-sizing: border-box;}
:host-context(.top) { margin-bottom: 0.5rem;}
:host-context(.bottom) { margin-top: 0.5rem;}
Styles to make a tooltip float are shipped by global styles, so I do not need to it on my own hands.
Info about the current position of the tooltip I took from CSS classes like top and bottom (defined in a panelClass
parameter in getPositionStrategy
function). Thanks to that, I can apply different styles if the tooltip is above or below the element.
Q&A
How to implement a fallback position strategy
@Directive({ selector: "[appTooltip]", standalone: true,})export class TooltipDirective implements OnDestroy { // ...
private getPositionStrategy(): PositionStrategy { return this.overlay .position() .flexibleConnectedTo(this.element) .withPositions([ { originX: "center", originY: "top", overlayX: "center", overlayY: "bottom", panelClass: "top", }, { originX: "center", originY: "bottom", overlayX: "center", overlayY: "top", panelClass: "bottom", }, ]); }}
As you can see, we have two positions (array with two elements). That means the overlay will try to put an element using the first position. When the element will not fit on the screen then the overlay will take the second position. If the second will not fit, the overlay will take the third one (and so on). When there are no more positions then the last one will be applied. All that means, if you want to define a fallback position strategy just add the new position into the end of the array.