Run the CLI
Use the CLI to add the component to your project.
npx @ngzard/ui add tooltipA popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
import { Component } from '@angular/core';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardTooltipModule } from '../tooltip';
@Component({
selector: 'z-demo-tooltip-hover',
standalone: true,
imports: [ZardButtonComponent, ZardTooltipModule],
template: ` <button z-button zType="outline" zTooltip="Tooltip content">Hover</button> `,
})
export class ZardDemoTooltipHoverComponent {}
Use the CLI to add the component to your project.
npx @ngzard/ui add tooltippnpm dlx @ngzard/ui add tooltipyarn dlx @ngzard/ui add tooltipbunx @ngzard/ui add tooltipCreate the component directory structure and add the following files to your project.
import { Overlay, OverlayModule, OverlayPositionBuilder, type OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { isPlatformBrowser, NgTemplateOutlet } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
type ComponentRef,
computed,
Directive,
ElementRef,
inject,
input,
NgModule,
type OnDestroy,
type OnInit,
output,
PLATFORM_ID,
Renderer2,
signal,
TemplateRef,
} from '@angular/core';
import { merge, Subject, take, takeUntil } from 'rxjs';
import { TOOLTIP_POSITIONS_MAP, type ZardTooltipPositions } from './tooltip-positions';
import { tooltipVariants } from './tooltip.variants';
import { mergeClasses } from '../../shared/utils/utils';
export type ZardTooltipTriggers = 'click' | 'hover';
@Directive({
selector: '[zTooltip]',
exportAs: 'zTooltip',
host: {
style: 'cursor: pointer',
},
})
export class ZardTooltipDirective implements OnInit, OnDestroy {
private readonly destroy$ = new Subject<void>();
private overlayPositionBuilder = inject(OverlayPositionBuilder);
private elementRef = inject(ElementRef);
private overlay = inject(Overlay);
private renderer = inject(Renderer2);
private platformId = inject(PLATFORM_ID);
private overlayRef?: OverlayRef;
private componentRef?: ComponentRef<ZardTooltipComponent>;
private scrollListenerRef?: () => void;
readonly zTooltip = input<string | TemplateRef<void> | null>(null);
readonly zPosition = input<ZardTooltipPositions>('top');
readonly zTrigger = input<ZardTooltipTriggers>('hover');
readonly zOnShow = output<void>();
readonly zOnHide = output<void>();
get nativeElement() {
return this.elementRef.nativeElement;
}
get overlayElement() {
return this.componentRef?.instance.elementRef.nativeElement;
}
ngOnInit() {
this.setTriggers();
if (isPlatformBrowser(this.platformId)) {
const positionStrategy = this.overlayPositionBuilder.flexibleConnectedTo(this.elementRef).withPositions([TOOLTIP_POSITIONS_MAP[this.zPosition()]]);
this.overlayRef = this.overlay.create({ positionStrategy });
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
show() {
if (this.componentRef) return;
const tooltipText = this.zTooltip();
if (!tooltipText) return;
const tooltipPortal = new ComponentPortal(ZardTooltipComponent);
this.componentRef = this.overlayRef?.attach(tooltipPortal);
if (!this.componentRef) return;
this.componentRef.instance.setProps(tooltipText, this.zPosition(), this.zTrigger());
this.componentRef.instance.state.set('opened');
this.componentRef.instance.onLoad$.pipe(take(1)).subscribe(() => {
this.zOnShow.emit();
switch (this.zTrigger()) {
case 'click':
if (!this.overlayRef) return;
this.overlayRef
.outsidePointerEvents()
.pipe(takeUntil(merge(this.destroy$, this.overlayRef.detachments())))
.subscribe(() => this.hide());
break;
case 'hover':
this.renderer.listen(
this.elementRef.nativeElement,
'mouseleave',
(event: Event) => {
event.preventDefault();
this.hide();
},
{ once: true },
);
break;
}
});
this.scrollListenerRef = this.renderer.listen(window, 'scroll', () => {
this.hide(0);
});
}
hide(animationDuration = 150) {
if (!this.componentRef) return;
this.componentRef.instance.state.set('closed');
setTimeout(() => {
this.zOnHide.emit();
this.overlayRef?.detach();
this.componentRef?.destroy();
this.componentRef = undefined;
if (this.scrollListenerRef) this.scrollListenerRef();
}, animationDuration);
}
private setTriggers() {
const showTrigger = this.zTrigger() === 'click' ? 'click' : 'mouseenter';
this.renderer.listen(this.elementRef.nativeElement, showTrigger, (event: Event) => {
event.preventDefault();
this.show();
});
}
}
@Component({
selector: 'z-tooltip',
imports: [NgTemplateOutlet],
template: `
@if (templateContent) {
<ng-container *ngTemplateOutlet="templateContent"></ng-container>
} @else if (stringContent) {
{{ stringContent }}
}
`,
host: {
'[class]': 'classes()',
'[attr.data-side]': 'position()',
'[attr.data-state]': 'state()',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardTooltipComponent implements OnInit, OnDestroy {
private readonly destroy$ = new Subject<void>();
readonly elementRef = inject(ElementRef);
protected position = signal<ZardTooltipPositions>('top');
private trigger = signal<ZardTooltipTriggers>('hover');
protected text: string | TemplateRef<void> | null = null;
state = signal<'closed' | 'opened'>('closed');
private onLoadSubject$ = new Subject<void>();
onLoad$ = this.onLoadSubject$.asObservable();
protected readonly classes = computed(() => mergeClasses(tooltipVariants()));
ngOnInit(): void {
this.onLoadSubject$.next();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.onLoadSubject$.complete();
}
setProps(text: string | TemplateRef<void> | null, position: ZardTooltipPositions, trigger: ZardTooltipTriggers) {
if (text) this.text = text;
this.position.set(position);
this.trigger.set(trigger);
}
get templateContent(): TemplateRef<void> | null {
return this.text instanceof TemplateRef ? this.text : null;
}
get stringContent(): string | null {
return typeof this.text === 'string' ? this.text : null;
}
}
@NgModule({
imports: [OverlayModule, ZardTooltipComponent, ZardTooltipDirective],
exports: [ZardTooltipComponent, ZardTooltipDirective],
})
export class ZardTooltipModule {}
import { cva, type VariantProps } from 'class-variance-authority';
export const tooltipVariants = cva(
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
);
export type ZardTooltipVariants = VariantProps<typeof tooltipVariants>;
import type { ConnectedPosition } from '@angular/cdk/overlay';
export const TOOLTIP_POSITIONS_MAP: { [key: string]: ConnectedPosition } = {
top: {
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom',
offsetY: -8,
},
bottom: {
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top',
offsetY: 8,
},
left: {
originX: 'start',
originY: 'center',
overlayX: 'end',
overlayY: 'center',
offsetX: -8,
},
right: {
originX: 'end',
originY: 'center',
overlayX: 'start',
overlayY: 'center',
offsetX: 8,
},
};
export type ZardTooltipPositions = 'top' | 'bottom' | 'left' | 'right';
import { Component } from '@angular/core';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardTooltipModule } from '../tooltip';
@Component({
selector: 'z-demo-tooltip-hover',
standalone: true,
imports: [ZardButtonComponent, ZardTooltipModule],
template: ` <button z-button zType="outline" zTooltip="Tooltip content">Hover</button> `,
})
export class ZardDemoTooltipHoverComponent {}
import { Component } from '@angular/core';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardTooltipModule } from '../tooltip';
@Component({
selector: 'z-demo-tooltip-click',
standalone: true,
imports: [ZardButtonComponent, ZardTooltipModule],
template: ` <button z-button zType="outline" zTooltip="Tooltip content" zTrigger="click">Click</button> `,
})
export class ZardDemoTooltipClickComponent {}
import { Component } from '@angular/core';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardTooltipModule } from '../tooltip';
@Component({
selector: 'z-demo-tooltip-position',
standalone: true,
imports: [ZardButtonComponent, ZardTooltipModule],
template: `
<div class="flex flex-col space-y-2">
<button z-button zType="outline" zTooltip="Tooltip content" zPosition="top">Top</button>
<div class="flex space-x-2">
<button z-button zType="outline" zTooltip="Tooltip content" zPosition="left">Left</button>
<button z-button zType="outline" zTooltip="Tooltip content" zPosition="right">Right</button>
</div>
<button z-button zType="outline" zTooltip="Tooltip content" zPosition="bottom">Bottom</button>
</div>
`,
})
export class ZardDemoTooltipPositionComponent {}
import { Component } from '@angular/core';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardTooltipModule } from '../tooltip';
@Component({
selector: 'z-demo-tooltip-events',
standalone: true,
imports: [ZardButtonComponent, ZardTooltipModule],
template: ` <button z-button zType="outline" zTooltip="Tooltip content" (zOnShow)="onShow()" (zOnHide)="onHide()">Events</button> `,
})
export class ZardDemoTooltipEventsComponent {
onShow() {
console.log('show');
}
onHide() {
console.log('hide');
}
}