Skip to content

Using the tooltip

@flyze/lib-core-angular 1.0.0-alpha.34· latest

How to show a tooltip on hover, from imperative code, or from your own template event.

Behavior and limitations: features/tooltip.

Before anything: the CDK overlay stylesheet

Section titled “Before anything: the CDK overlay stylesheet”

The panel is positioned by @angular/cdk/overlay. Without its structural CSS the tooltip renders in the document flow instead of next to its anchor — no error, just a tooltip in the wrong place.

If your app has a Material theme you already have them, because mat.core() includes @include cdk.overlay(). If it does not, add this once, globally:

@import '@angular/cdk/overlay-prebuilt.css';

This library does not ship those rules, so nothing warns you.

FyzTooltip is a standalone directive, so import the class — there is no module:

@Component({
standalone: true,
imports: [FyzTooltip],
})

In an NgModule-based component, add FyzTooltip to the module’s imports, not declarations.

<button [fyzTooltip]="'Saves the current document'">Save</button>

The text is the input value, so an empty string suppresses the tooltip entirely — convenient when the text comes from data that may be missing:

<button [fyzTooltip]="row.hint">{{ row.label }}</button>

Change the text while the tooltip is open and the open panel follows it, repositioning as it resizes.

<button [fyzTooltip]="'Waited for this'" [fyzTooltipShowDelay]="600" [fyzTooltipHideDelay]="1500">
Delayed
</button>

fyzTooltipShowDelay stops a pointer that is only passing through from flashing a tooltip on everything it crosses. fyzTooltipHideDelay lets a pointer that slips off an edge and comes straight back keep the tooltip it already had — re-entering within the delay keeps the same panel rather than building a second one.

<button [fyzTooltip]="'Only while enabled'" [fyzTooltipDisabled]="!canSave">Save</button>

Setting fyzTooltipDisabled while a tooltip is open closes that one too, immediately, regardless of fyzTooltipHideDelay.

The panel is rendered into .cdk-overlay-container at the end of <body>, so a class passed through fyzTooltipClass must resolve globally — a component-scoped rule will not match it.

<button [fyzTooltip]="'Custom'" [fyzTooltipClass]="'my-tooltip--wide'">Hover</button>
<button [fyzTooltip]="'Two classes'" [fyzTooltipClass]="['my-tooltip--wide', 'my-tooltip--loud']">
Hover
</button>
// in a global stylesheet, not a component's
.my-tooltip--wide {
--fyz-tooltip-max-width: 420px;
}

Prefer the --fyz-tooltip-* custom properties over overriding declarations directly: the panel’s own rules carry its encapsulation attribute, so a bare class loses the specificity contest and you end up reaching for !important. See features/tooltip for the full list.

Use FyzTooltipService when the anchor is your own host element. A directive cannot be applied to your own host from inside your own template, so a component in that position holds an HTMLElement and nothing else.

attach() is the method to reach for. It wires the hover, resolves its options on every one, and returns the teardown:

@Component({ selector: 'my-button', template: `<button>{{ label }}</button>` })
export class MyButtonComponent implements OnDestroy {
@Input() label = '';
@Input() hint = '';
readonly #stopTooltip: () => void;
constructor(host: ElementRef<HTMLElement>, tooltips: FyzTooltipService) {
// A factory, not a fixed object: `hint` is an input, so it is not set yet at construction and
// can change afterwards. Re-read on every hover; nullish suppresses that hover.
this.#stopTooltip = tooltips.attach(host, () => (this.hint ? { text: this.hint } : null));
}
ngOnDestroy() {
this.#stopTooltip();
}
}

Call the stop function from ngOnDestroy. It removes the listeners and disposes a panel that may still be on screen — skip it and a tooltip outlives the element it was pointing at. It is idempotent, so calling it twice is safe.

Pass a plain object instead of a factory when nothing changes:

tooltips.attach(host, { text: 'Static', showDelay: 300 });
tooltips.show(host, { text: 'Opened from code' });
tooltips.hide(host); // honours the stored hideDelay
tooltips.hide(host, 0); // override it for this call

Ask the service rather than tracking state yourself — it knows about closes you did not initiate:

protected get isOpen(): boolean {
return this.tooltips.isOpen(this.anchor);
}
protected toggle() {
this.isOpen
? this.tooltips.hide(this.anchor)
: this.tooltips.show(this.anchor, { text: 'Toggled' });
}

Every method takes HTMLElement or ElementRef<HTMLElement>, so you can pass a @ViewChild ref directly.

Two more, for the awkward cases:

  • hideImmediately(host) — no animation, no delay. For a teardown path of your own.
  • hideAll() — closes every open panel whichever host it belongs to, ignoring each one’s delay. The escape hatch when you have lost track of your anchors, such as a route change.

If you call show() yourself, you own the teardown. attach() exists so that you usually do not.

A third way in, with no service injected and no hover:

<button #tip="fyzTooltip" [fyzTooltip]="'Opened from the template'">Anchor</button>
<button (click)="tip.show()">show</button>
<button (click)="tip.hide()">hide</button>

show() reads the same inputs a hover would, so fyzTooltipDisabled and an empty text still suppress it.

The /fyz-tooltip route of the playground app covers all of the above, including the cases only a browser can show — clicking straight through a panel, long text wrapping, a panel near the viewport edge, and light/dark theming. See the root README for running it.