@ngneat/helipopper vs ngx-popperjs
Angular Popper.js Integrations
@ngneat/helipopperngx-popperjsSimilar Packages:

Angular Popper.js Integrations

Both @ngneat/helipopper and ngx-popperjs are Angular libraries designed to facilitate the integration of Popper.js, a powerful positioning engine for tooltips and popovers. They provide developers with the ability to create dynamic and responsive UI elements that can be positioned relative to other elements on the page. While both libraries serve a similar purpose, they differ in terms of features, ease of use, and customization options, making them suitable for different types of projects and developer preferences.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@ngneat/helipopper0453158 kB2412 hours agoMIT
ngx-popperjs067253 kB0a year agoMIT

Feature Comparison: @ngneat/helipopper vs ngx-popperjs

API Design

  • @ngneat/helipopper:

    @ngneat/helipopper provides a more modern and Angular-centric API that leverages Angular's reactive programming principles. It allows developers to easily manage the lifecycle of poppers through Angular's dependency injection and change detection, resulting in a more intuitive integration with Angular applications.

  • ngx-popperjs:

    ngx-popperjs offers a simpler API that is easy to understand and implement. It focuses on providing essential features for tooltips and popovers without overwhelming developers with advanced options, making it suitable for quick implementations.

Customization Options

  • @ngneat/helipopper:

    @ngneat/helipopper allows for extensive customization of popper behavior and appearance. Developers can easily modify the positioning, styling, and animations of poppers, providing greater control over the user experience.

  • ngx-popperjs:

    ngx-popperjs offers basic customization options but may be limited compared to @ngneat/helipopper. It is suitable for developers who need standard tooltip and popover features without the need for deep customization.

Integration with Angular Features

  • @ngneat/helipopper:

    This package is designed to work seamlessly with Angular's reactive features, such as observables and change detection. This makes it easier to manage dynamic content and updates, ensuring that poppers behave correctly in response to application state changes.

  • ngx-popperjs:

    While ngx-popperjs can be integrated with Angular, it does not leverage Angular's reactive features as effectively as @ngneat/helipopper. This may lead to more manual management of state and updates.

Performance

  • @ngneat/helipopper:

    @ngneat/helipopper is optimized for performance in Angular applications, ensuring that poppers are rendered efficiently and only when necessary. This can lead to improved application responsiveness, especially in complex UIs.

  • ngx-popperjs:

    ngx-popperjs provides decent performance but may not be as optimized for Angular's change detection and lifecycle management, potentially leading to unnecessary re-renders in certain scenarios.

Documentation and Community Support

  • @ngneat/helipopper:

    @ngneat/helipopper has comprehensive documentation and an active community, making it easier for developers to find resources, examples, and support when implementing the library in their projects.

  • ngx-popperjs:

    ngx-popperjs has basic documentation, which may be sufficient for simple use cases but might lack depth for more complex implementations. The community support may also be less active compared to @ngneat/helipopper.

How to Choose: @ngneat/helipopper vs ngx-popperjs

  • @ngneat/helipopper:

    Choose @ngneat/helipopper if you need a more modern and flexible API that integrates seamlessly with Angular's reactive programming model. It offers better support for Angular features like observables and change detection, making it ideal for applications that require dynamic positioning and responsiveness.

  • ngx-popperjs:

    Choose ngx-popperjs if you prefer a straightforward implementation with a focus on simplicity and ease of use. It is suitable for projects that require basic tooltip and popover functionality without the need for extensive customization or advanced features.

README for @ngneat/helipopper


MIT commitizen PRs styled with prettier All Contributors ngneat spectator @ngneat/helipopper

A Powerful Tooltip and Popover for Angular Applications

Tippy.js is the complete tooltip, popover, dropdown, and menu solution for the web, powered by Popper.js.

It is an abstraction over Popper that provides the logic and optionally the styling involved in all types of elements that pop out from the flow of the document and get overlaid on top of the UI, positioned next to a reference element.

This is a lightweight wrapper with additional features that lets you use it declaratively in Angular. Tippy has virtually no restrictions over Popper and gives you limitless control while providing useful behavior and defaults.

If you're using v1 and don't want to migrate, you can find it here.

Features

✅ Position Tooltips, Menus, Dropdowns, and Popovers
✅ Predefined Variations
✅ TemplateRef/Component Support
✅ Lazy Registration
✅ Manual Trigger Support
✅ Text Overflow Support
✅ Context Menu Support
✅ Component Bindings via inputBinding / outputBinding / twoWayBinding
✅ Global Enable / Disable All

Installation

$ npm i @ngneat/helipopper
# Or if you're using yarn
$ yarn add @ngneat/helipopper
# Or if you're using pnpm
$ pnpm i @ngneat/helipopper

Configure it as shown below:

import { provideTippyLoader, provideTippyConfig, tooltipVariation, popperVariation } from '@ngneat/helipopper/config';

bootstrapApplication(AppComponent, {
  providers: [
    provideTippyLoader(() => import('tippy.js')),
    provideTippyConfig({
      defaultVariation: 'tooltip',
      variations: {
        tooltip: tooltipVariation,
        popper: popperVariation,
      },
    }),
  ],
});

Please note that the provideTippyLoader is required, as it specifies how Tippy is loaded - either synchronously or asynchronously. When dynamic import is used, the library will load only when the first Tippy directive is rendered. If we want it to load synchronously, we use the following:

import tippy from 'tippy.js';

provideTippyLoader(() => tippy);

Add the styles you want to styles.scss:

@import 'tippy.js/dist/tippy.css';
@import 'tippy.js/themes/light.css';
@import 'tippy.js/animations/scale.css';

You have the freedom to customize it if you need to.

Import the standalone TippyDirective in your components:

import { TippyDirective } from '@ngneat/helipopper';

@Component({
  standalone: true,
  imports: [TippyDirective],
})
class ExampleComponent {}

And use it in your templates:

<button tp="Helpful Message">I have a tooltip</button>

The library exposes default variations for tooltip and popper. You can use them, extend them, or pass your own variations. A variation is a set of predefined tippy properties. For example, here's how the built-in tooltip variation looks like:

export const tooltipVariation = {
  theme: null,
  arrow: false,
  animation: 'scale',
  trigger: 'mouseenter',
  offset: [0, 5],
};

Use TemplateRef as content

<button [tp]="tpl" tpVariation="popper">Click Me</button>

<ng-template #tpl let-hide>
  <h6>Popover title</h6>
  <p>And here's some amazing content. It's very engaging. Right?</p>
</ng-template>

Use Component as content

import type { TippyInstance } from '@ngneat/helipopper/config';
import { injectTippyRef } from '@ngneat/helipopper';

@Component()
class MyComponent {
  tippy = injectTippyRef();
}
<button [tp]="MyComponent">Click Me</button>

Use a Lazy Factory to Resolve a Component

Pass a factory function that returns a Promise<Type<any>> to defer loading the component until the tooltip is first shown. This is ideal for code-splitting — the component's chunk is only fetched on demand:

@Component({})
class HostComponent {
  lazyComponent = () => import('./heavy-popover.component').then(m => m.HeavyPopoverComponent);
}
<button [tp]="lazyComponent" tpVariation="popper">Click Me</button>

The factory is called once; after the component resolves, subsequent shows reuse the already-loaded class.

provideTippyLoader accepts optional feature functions as additional arguments to control what happens while the lazy component is being fetched:

withTippyLoaderComponent

Renders a placeholder component inside the tooltip while the import is in flight:

import { provideTippyLoader, withTippyLoaderComponent } from '@ngneat/helipopper/config';

bootstrapApplication(AppComponent, {
  providers: [
    provideTippyLoader(
      () => import('tippy.js'),
      withTippyLoaderComponent(SpinnerComponent),
    ),
  ],
});

SpinnerComponent is mounted as the tooltip content the moment the tooltip opens, then replaced by the resolved component once the import completes.

withTippyLoaderTiming

Controls the minimum time the loader is visible. The default is a synchronous observable (of(undefined)) that lets the swap happen as soon as the import resolves. Pass a factory — with full DI support — to impose a minimum display duration:

import { provideTippyLoader, withTippyLoaderTiming } from '@ngneat/helipopper/config';
import { inject } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [
    provideTippyLoader(
      () => import('tippy.js'),
      withTippyLoaderComponent(SpinnerComponent),
      withTippyLoaderTiming(() => {
        const scheduler = inject(Scheduler);
        return new Observable(subscriber => {
          scheduler.schedule(() => {
            subscriber.next();
            subscriber.complete();
          }, 300);
        });
      }),
    ),
  ],
});

The tooltip swaps the loader for the real component only when both the import has resolved and the timing observable has emitted — so the spinner is guaranteed to be visible for at least the configured duration regardless of how fast the import finishes.

Component Bindings

Pass reactive Angular bindings (inputBinding, outputBinding, twoWayBinding) to a dynamically created component using the tpBindings input:

import { inputBinding, signal } from '@angular/core';
import { MyPopoverComponent } from './my-popover.component';

@Component({})
class HostComponent {
  component = MyPopoverComponent;
  readonly greeting = signal('Hello!');
  readonly bindings = [
    inputBinding('greeting', () => this.greeting()),
  ];
}
<button [tp]="component" tpVariation="popper" [tpBindings]="bindings">Open</button>

The same bindings option is available when creating a tooltip programmatically via TippyService:

import { inputBinding } from '@angular/core';

this.tippyService.create(host, MyPopoverComponent, {
  bindings: [inputBinding('greeting', () => this.greeting())],
}).subscribe((instance) => { ... });

Host directives can be applied the same way using tpDirectives / directives.

Text Overflow

You can pass the onlyTextOverflow input to show the tooltip only when the host overflows its container:

<div style="max-width: 100px;" class="overflow-hidden flex">
  <p class="ellipsis" [tp]="text" tpPlacement="right" [tpOnlyTextOverflow]="true">
    {{ text }}
  </p>
</div>

Note: this feature is using ResizeObserver api.

You might have cases where the host has a static width and the content is dynamic. In this case, use the tpStaticWidthHost input with combination with tpOnlyTextOverflow:

<div style="max-width: 100px;" class="overflow-hidden flex">
  <p
    style="width: 100px"
    class="ellipsis"
    [tp]="dynamicText"
    tpPlacement="right"
    [tpOnlyTextOverflow]="true"
    tpStaticWidthHost
  >
    {{ dynamicText }}
  </p>
</div>

Note: when using tpStaticWidthHost you can't use tpUseTextContent, you need to explicitly pass the content to tp in order to trigger content change.

Use Text Content

You can instruct tippy to use the element textContent as the tooltip content:

<p tp tpUseTextContent>{{ text }}</p>

Lazy

You can pass the tpIsLazy input when you want to defer the creation of tippy only when the element is in the view:

<div *ngFor="let item of items" [tp]="item.label" [tpIsLazy]="true">{{ item.label }}</div>

Note that it's using IntersectionObserver api.

Context Menu

First, define the contextMenu variation:

import {
  popperVariation,
  tooltipVariation,
  provideTippyConfig,
  withContextMenuVariation,
} from '@ngneat/helipopper/config';

bootstrapApplication(AppComponent, {
  providers: [
    provideTippyConfig({
      defaultVariation: 'tooltip',
      variations: {
        tooltip: tooltipVariation,
        popper: popperVariation,
        contextMenu: withContextMenuVariation(popperVariation),
      },
    }),
  ],
});

Now you can use it in your template:

<ng-template #contextMenu let-hide let-item="data">
  <ul>
    <li (click)="copy(item); hide()">Copy</li>
    <li (click)="duplicate(item); hide()">Duplicate</li>
  </ul>
</ng-template>

<ul>
  <li
    *ngFor="let item of list"
    [tp]="contextMenu"
    [tpData]="item"
    tpVariation="contextMenu"
  >
    {{ item.label }}
  </li>
</ul>

Manual Trigger

<div tp="Helpful Message" tpTrigger="manual" #tooltip="tippy">Click Open to see me</div>

<button (click)="tooltip.show()">Open</button>
<button (click)="tooltip.hide()">Close</button>

Declarative show/hide

Use isVisible to trigger show and hide. Set trigger to manual.

<div tp="Helpful Message" tpTrigger="manual" [tpIsVisible]="visibility">
  Click Open to see me
</div>

<button (click)="visibility = true">Open</button>
<button (click)="visibility = false">Close</button>

You can see more examples in our playground, or live here.

Inputs

tp: string | TemplateRef<any> | Type<any> | (() => Promise<Type<any>>) | undefined | null;
tpAppendTo: TippyProps['appendTo'];
tpDelay: TippyProps['delay'];
tpDuration: TippyProps['duration'];
tpHideOnClick: TippyProps['hideOnClick'];
tpInteractive: TippyProps['interactive'];
tpInteractiveBorder: TippyProps['interactiveBorder'];
tpMaxWidth: TippyProps['maxWidth'];
tpOffset: TippyProps['offset'];
tpPlacement: TippyProps['placement'];
tpPopperOptions: TippyProps['popperOptions'];
tpShowOnCreate: TippyProps['showOnCreate'];
tpTrigger: TippyProps['trigger'];
tpTriggerTarget: TippyProps['triggerTarget'];
tpZIndex: TippyProps['zIndex'];
tpAnimation: TippyProps['animation'];
tpUseTextContent: boolean;
tpIsLazy: boolean;
tpVariation: string;
tpIsEnabled: boolean;
tpIsVisible: boolean;
tpClassName: string;
tpOnlyTextOverflow: boolean;
tpStaticWidthHost: boolean;
tpData: any;
tpUseHostWidth: boolean;
tpHideOnEscape: boolean;
tpPopperWidth: number | string;
tpHost: HTMLElement;
tpBindings: Binding[];       // inputBinding / outputBinding / twoWayBinding descriptors
tpDirectives: DirectiveWithBindings[]; // host directives with optional bindings

Outputs

tpVisible = new EventEmitter<boolean>();

Global Config

  • You can pass any tippy option at global config level.
  • beforeRender - Hook that'll be called before rendering the tooltip content ( applies only for string )

Create tippy Programmatically

import type { TippyInstance } from '@ngneat/helipopper/config';
import { TippyService } from '@ngneat/helipopper';

class Component {
  @ViewChild('inputName') inputName: ElementRef;
  tippy: TippyInstance;
  private tippyService = inject(TippyService);

  async show() {
    if (!this.tippy) {
      this.tippy = await firstValueFrom(
        this.tippyService.create(this.inputName, 'this field is required')
      );
    }

    this.tippy.show();
  }

  ngOnDestroy() {
    this.tippy?.destroy();
  }
}

Enable / Disable All Tooltips

TippyService exposes enableAll() and disableAll() methods that globally enable or disable every tooltip managed by the service:

class MyComponent {
  private tippyService = inject(TippyService);

  disableTooltips() {
    this.tippyService.disableAll();
  }

  enableTooltips() {
    this.tippyService.enableAll();
  }
}

Tooltips that were individually disabled via [tpIsEnabled]="false" are not re-enabled by enableAll().

Contributors ✨

Thank goes to all these wonderful people who contributed ❤️