How to create a tooltip in Angular
Tooltips are essential UI components that provide contextual information when users hover over or focus on elements without cluttering the interface. With over 10 years of experience building Angular applications since 2014 and as the creator of CoreUI, a widely used open-source UI library, I’ve implemented countless tooltips in production applications. The most robust and accessible approach is to use Angular CDK Overlay, which handles positioning, backdrop management, and accessibility automatically. This method provides flexibility in styling and behavior while ensuring proper focus management and keyboard support.
Use Angular CDK Overlay with a tooltip directive to create accessible, well-positioned tooltips with flexible trigger options.
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core'
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { TooltipComponent } from './tooltip.component'
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
@Input('appTooltip') text = ''
private overlay = inject(Overlay)
private elementRef = inject(ElementRef)
private overlayRef: OverlayRef | null = null
@HostListener('mouseenter')
show() {
if (this.overlayRef) {
return
}
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' }
])
this.overlayRef = this.overlay.create({ positionStrategy })
const portal = new ComponentPortal(TooltipComponent)
const componentRef = this.overlayRef.attach(portal)
componentRef.instance.text = this.text
}
@HostListener('mouseleave')
hide() {
if (this.overlayRef) {
this.overlayRef.dispose()
this.overlayRef = null
}
}
}
This tooltip directive uses Angular CDK Overlay to create tooltips that appear on hover. The flexibleConnectedTo() method positions the tooltip relative to the host element, and the position configuration places it above the element by default. The mouseenter and mouseleave listeners show and hide the tooltip automatically. The directive passes the tooltip text to the tooltip component through the text input property.
Creating the Tooltip Component
import { Component, Input } from '@angular/core'
import { CommonModule } from '@angular/common'
@Component({
selector: 'app-tooltip',
standalone: true,
imports: [CommonModule],
template: `
<div class="tooltip-container">
{{ text }}
</div>
`,
styles: [`
.tooltip-container {
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 6px 12px;
border-radius: 4px;
font-size: 14px;
max-width: 250px;
word-wrap: break-word;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: none;
margin-bottom: 8px;
}
`]
})
export class TooltipComponent {
@Input() text = ''
}
The tooltip component renders the actual tooltip content with appropriate styling. The dark background with white text provides good contrast and readability. The pointer-events: none prevents the tooltip from interfering with mouse events, and the margin creates proper spacing from the host element. The component accepts the tooltip text as an input property, making it reusable for any content.
Using the Tooltip Directive
import { Component } from '@angular/core'
import { TooltipDirective } from './tooltip.directive'
@Component({
selector: 'app-example',
standalone: true,
imports: [TooltipDirective],
template: `
<div>
<button appTooltip="This is a helpful tooltip">Hover Me</button>
<span appTooltip="More information here">Info Icon</span>
<input appTooltip="Enter your email address" placeholder="Email">
</div>
`
})
export class ExampleComponent {}
Using the tooltip is simple: add the appTooltip directive to any element and provide the tooltip text as the directive value. The tooltip automatically appears when users hover over the element and disappears when they move away. This pattern works with any HTML element, including buttons, spans, inputs, and custom components.
Adding Multiple Position Options
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core'
import { Overlay, OverlayRef, ConnectionPositionPair } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { TooltipComponent } from './tooltip.component'
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
@Input('appTooltip') text = ''
@Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top'
private overlay = inject(Overlay)
private elementRef = inject(ElementRef)
private overlayRef: OverlayRef | null = null
private getPositions(): ConnectionPositionPair[] {
switch (this.tooltipPosition) {
case 'top':
return [{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom'
}]
case 'bottom':
return [{
originX: 'center',
originY: 'bottom',
overlayX: 'center',
overlayY: 'top'
}]
case 'left':
return [{
originX: 'start',
originY: 'center',
overlayX: 'end',
overlayY: 'center'
}]
case 'right':
return [{
originX: 'end',
originY: 'center',
overlayX: 'start',
overlayY: 'center'
}]
default:
return [{
originX: 'center',
originY: 'top',
overlayX: 'center',
overlayY: 'bottom'
}]
}
}
@HostListener('mouseenter')
show() {
if (this.overlayRef) {
return
}
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions(this.getPositions())
this.overlayRef = this.overlay.create({ positionStrategy })
const portal = new ComponentPortal(TooltipComponent)
const componentRef = this.overlayRef.attach(portal)
componentRef.instance.text = this.text
}
@HostListener('mouseleave')
hide() {
if (this.overlayRef) {
this.overlayRef.dispose()
this.overlayRef = null
}
}
}
This enhanced directive supports four positioning options: top, bottom, left, and right. The getPositions() method returns the appropriate position configuration based on the tooltipPosition input. Each position specifies where the tooltip should appear relative to the host element. You can now control tooltip placement by adding the tooltipPosition attribute to elements.
Adding Delay and Custom Triggers
import { Directive, ElementRef, HostListener, Input, inject } from '@angular/core'
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { TooltipComponent } from './tooltip.component'
@Directive({
selector: '[appTooltip]',
standalone: true
})
export class TooltipDirective {
@Input('appTooltip') text = ''
@Input() tooltipDelay = 500
@Input() tooltipTrigger: 'hover' | 'click' = 'hover'
private overlay = inject(Overlay)
private elementRef = inject(ElementRef)
private overlayRef: OverlayRef | null = null
private timeoutId: any = null
@HostListener('mouseenter')
onMouseEnter() {
if (this.tooltipTrigger === 'hover') {
this.timeoutId = setTimeout(() => this.show(), this.tooltipDelay)
}
}
@HostListener('mouseleave')
onMouseLeave() {
if (this.tooltipTrigger === 'hover') {
clearTimeout(this.timeoutId)
this.hide()
}
}
@HostListener('click')
onClick() {
if (this.tooltipTrigger === 'click') {
if (this.overlayRef) {
this.hide()
} else {
this.show()
}
}
}
private show() {
if (this.overlayRef) {
return
}
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' }
])
this.overlayRef = this.overlay.create({ positionStrategy })
const portal = new ComponentPortal(TooltipComponent)
const componentRef = this.overlayRef.attach(portal)
componentRef.instance.text = this.text
}
private hide() {
if (this.overlayRef) {
this.overlayRef.dispose()
this.overlayRef = null
}
}
}
This version adds a configurable delay before showing the tooltip, preventing tooltips from appearing during quick mouse movements. The tooltipTrigger input allows switching between hover and click triggers, providing flexibility for different use cases. The delay only applies to hover triggers, while click triggers toggle the tooltip immediately. The timeout is cleared if the mouse leaves before the delay expires, ensuring tooltips only appear when users pause on elements.
Creating Tooltips with HTML Content
import { Component, Input } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
@Component({
selector: 'app-tooltip',
standalone: true,
imports: [CommonModule],
template: `
<div class="tooltip-container" [innerHTML]="sanitizedContent"></div>
`,
styles: [`
.tooltip-container {
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
max-width: 300px;
word-wrap: break-word;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: none;
margin-bottom: 8px;
}
.tooltip-container :deep(strong) {
color: #ffd700;
}
.tooltip-container :deep(a) {
color: #64b5f6;
text-decoration: underline;
}
`]
})
export class TooltipComponent {
@Input() text = ''
@Input() allowHtml = false
constructor(private sanitizer: DomSanitizer) {}
get sanitizedContent(): SafeHtml | string {
if (this.allowHtml) {
return this.sanitizer.sanitize(1, this.text) || this.text
}
return this.text
}
}
For richer tooltips, the component can accept HTML content through the innerHTML binding. The DomSanitizer sanitizes the HTML to prevent XSS attacks while allowing safe formatting tags. The allowHtml input controls whether HTML is rendered or displayed as plain text. The :deep() selector styles elements within the HTML content, like making links blue and bold text yellow. This approach enables tooltips with formatted text, links, and other HTML elements.
Creating a Tooltip Service
import { Injectable, inject, ComponentRef } from '@angular/core'
import { Overlay, OverlayRef } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { TooltipComponent } from './tooltip.component'
@Injectable({
providedIn: 'root'
})
export class TooltipService {
private overlay = inject(Overlay)
private overlayRef: OverlayRef | null = null
show(element: HTMLElement, text: string, position = 'top') {
if (this.overlayRef) {
return
}
const positionStrategy = this.overlay
.position()
.flexibleConnectedTo(element)
.withPositions([
{ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom' }
])
this.overlayRef = this.overlay.create({
positionStrategy,
scrollStrategy: this.overlay.scrollStrategies.reposition()
})
const portal = new ComponentPortal(TooltipComponent)
const componentRef = this.overlayRef.attach(portal)
componentRef.instance.text = text
}
hide() {
if (this.overlayRef) {
this.overlayRef.dispose()
this.overlayRef = null
}
}
}
A tooltip service provides programmatic control over tooltips, allowing you to show and hide them from component logic rather than through directives. This is useful for dynamic tooltips that appear based on application state or user actions. The service accepts a DOM element reference, tooltip text, and position, creating the tooltip at the specified location. The scroll strategy ensures the tooltip repositions when the page scrolls, maintaining proper alignment.
Best Practice Note
For production applications, consider using pre-built tooltip components from CoreUI for Angular, which provide consistent styling, accessibility features, and advanced positioning out of the box. You can explore the CoreUI Angular Tooltip documentation for production-ready tooltip components that follow best practices. For more advanced overlay patterns, check out how to create a custom overlay in Angular and how to create a modal in Angular. This is the same pattern we use in CoreUI components to ensure accessible and well-positioned tooltips across all applications.



