PureType

Tooltips in Phoenix LiveView

There are a few options to integrate tooltip functionality into Phoenix LiveView. This article covers integrating a Phoenix LiveView with one popular library tippy.js, ensuring any updates from LiveView are reflected in tooltip state.

Background

tippy.js is a powerful, versatile and lightweight Javascript tooltip library. It provides the logic (and optional styling) of elements that “pop out” and float next to a target element. Although the last version was released three years ago, it receives over a million downloads a week. The approach here can be transferred to other libraries like Floating UI.

With Phoenix LiveView applications, we may wish to have the content of tooltips (and their styling) vary depending on the current application state.

Because tippy.js takes care of configuring and rendering the tooltip, and stores internal state outside of the element, we’ll need to use a LiveView client hook to ensure updates to the element are reflected in the tippy.js internal state associated with the element.

(Need a refresher on how to integrate client-side libraries that take care of rendering? See this previous article: Phoenix LiveView, hooks, and push-event: json-view)

Installation

In a Phoenix LiveView application, let's install the library:

1npm install --prefix assets tippy.js

We’ll need to now include the Javascript and (default) CSS in separate ways.

For the Javascript code - We can then include tippy.js in assets/js/app.js, with the hook to come:

1import tippy from "tippy.js"

And the CSS can be imported in assets/css/app.css:

1@import "tippy.js/dist/tippy.css";

Configuring tooltips (tippy.js)

With this library now included in our application, we can now invoke tippy, a function that creates tooltip context associated with a particular element. We can use the returned value (an instance) to control, update and invoke the tooltip as necessary.

tippy.js includes a number of configurable properties (with associated defaults) - the documentation covers all of these.

We can keep the need for custom Javascript code very light, and integrate with LiveView well, by using data-tippy attributes. We can access these properties via the dataset attribute, which we’ll do so in the hook in the following section.

When creating the tooltip instance, the tippy function actually reads these data-tippy elements. However, if we update those attribute values with LiveView, it won’t update the tooltip. This is a limitation of the library, documented in the FAQ.

So, keeping LiveView’s update mechanism in mind, we’ll have to write a hook to create the tooltip instance when the element is mounted, and update the properties correctly when the element is updated.

Let’s take a look at the element first, to see what kind of state we’ll need to deal with.

The HTML element

Here’s a button element that will integrate with the hook we’re about to write:

1<button id="example-button" phx-hook="Tippy" data-tippy-content="Hello!" data-tippy-placement="bottom">
2 Hover over me
3</button>

There are several things to note here:

  • id is required for all elements that have a phx-hook associated with them, so Phoenix LiveView can associate internal state by element ID

  • phx-hook associates a particular hook. Only (at most) one hook is permitted per element

  • data-tippy- is the prefix for each attribute. We’ll need to process these dataset elements (see the dataset property documentation) to pass a configuration object when we come to update the properties associated with the tooltip. In short: data-tippy-placement=”bottom” in the DOM becomes this.el.dataset.tippyPlacement // => “bottom” in Javascript.

The LiveView hook

Here’s the code for the hook. Let’s go through the below section by section.

1const Hooks = {
2 Tippy: {
3 mounted() {
4 this.instance = tippy(this.el);
5 },
6 updated() {
7 this.instance.setProps(
8 Object.fromEntries(
9 Object.entries(this.el.dataset)
10 .filter(([k]) => k.startsWith("tippy"))
11 .map(([k, v]) => [
12 this.tippyPropName(k),
13 v
14 ])
15 )
16 );
17 },
18 destroyed() {
19 this.instance.destroy();
20 },
21
22 tippyPropName(k) {
23 const strippedName = k.replace("tippy", "");
24 return strippedName.charAt(0).toLowerCase() + strippedName.slice(1);
25 },
26 },
27};

mounted()

When the element is first added to the page, the mounted() callback is invoked. We’ll need to create the tooltip instance associated with the DOM element (this.el):

1this.instance = tippy(this.el);

After this, tippy.js takes care of attaching to the relevant DOM events and invoking the logic to display the tooltip.

updated()

As noted above, tippy.js doesn’t update its internal state if the values of attributes change.

1this.instance.setProps(
2 Object.fromEntries(
3 Object.entries(this.el.dataset)
4 .filter(([k]) => k.startsWith("tippy"))
5 .map(([k, v]) => [
6 this.tippyPropName(k),
7 v
8 ])
9 )
10);
11

Step by step:

  • It starts with this.el.dataset, which contains all data attributes of an HTML element.

  • It filters these attributes to only include those that start with "tippy" (data-tippy in the HTML)

  • For each filtered attribute, it transforms the attribute name into a valid Tippy.js property name using a tippyPropName method.

  • It creates an object from these transformed key-value pairs.

    Finally, it sets these properties on the Tippy.js instance using setProps().

Another alternative: we could invoke tippy again to create another tooltip. But this has the clearest purpose and intention, even if the logic to transform attribute names isn’t necessarily easy to parse.

destroyed()

Without this, tippy.js tooltip instances accumulate during LiveView updates, causing memory leaks.

tippyPropName(k)

We need to remove the tippy prefix from the property name, and convert the casing of the remaining string (from a leading uppercase character to a lower one). So tippyShowOnCreate becomes showOnCreate, the object key that the setProps method expects.

Alternatives

tippy.js is by no means the only tooltip library that can be integrated with Phoenix LiveView:

  • alpine-tooltip is a popular plugin for Alpine.js, commonly used as a simple Javascript dynamic layer in Phoenix LiveView applications

  • Floating UI is a library (superseding popper.js) that helps you create “floating” elements such as tooltips, popovers, dropdowns and more. Floating UI is framewor

  • Use Phoenix LiveView component libraries like salad_ui or Chelekom, which contain their own tooltip component implementations.

Conclusion

Integrating tippy.js with Phoenix LiveView provides a powerful and flexible solution for implementing tooltips in your application. By utilizing a LiveView client hook, we can ensure that any updates to the tooltip content or styling are seamlessly reflected in the tippy.js internal state.

This approach allows for dynamic, state-dependent tooltips that respond to changes in the application's context, enhancing the user experience and interactivity of your Phoenix LiveView application.

While tippy.js is a popular choice, it's worth noting that there are alternative libraries and approaches available for implementing tooltips in Phoenix LiveView. Whether you choose tippy.js, Alpine.js plugins, Floating UI, or Phoenix-specific component libraries, the key is to find a solution that best fits your project's needs and integrates smoothly with LiveView's update mechanism.