<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: PureType</title>
    <description>The latest articles on Forem by PureType (@puretype).</description>
    <link>https://forem.com/puretype</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9957%2F8e66781b-3345-4125-95ef-87628f1a2ca8.png</url>
      <title>Forem: PureType</title>
      <link>https://forem.com/puretype</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/puretype"/>
    <language>en</language>
    <item>
      <title>Tooltips in Phoenix LiveView</title>
      <dc:creator>Matt Whitworth</dc:creator>
      <pubDate>Thu, 13 Feb 2025 19:45:06 +0000</pubDate>
      <link>https://forem.com/puretype/tooltips-in-phoenix-liveview-k8e</link>
      <guid>https://forem.com/puretype/tooltips-in-phoenix-liveview-k8e</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;(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)&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;In a Phoenix LiveView application, let's install the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--prefix&lt;/span&gt; assets tippy.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ll need to now include the Javascript and (default) CSS in separate ways.&lt;/p&gt;

&lt;p&gt;For the Javascript code - We can then include tippy.js in assets/js/app.js, with the hook to come:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tippy&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tippy.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the CSS can be imported in assets/css/app.css:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tippy.js/dist/tippy.css"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring tooltips (tippy.js)
&lt;/h2&gt;

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

&lt;p&gt;tippy.js includes a number of configurable properties (with associated defaults) - &lt;a href="https://atomiks.github.io/tippyjs/v6/all-props/" rel="noopener noreferrer"&gt;the documentation&lt;/a&gt; covers all of these.&lt;/p&gt;

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

&lt;p&gt;When creating the tooltip instance, the &lt;code&gt;tippy&lt;/code&gt; function actually reads these &lt;code&gt;data-tippy&lt;/code&gt; elements. However, if we update those attribute values with LiveView, it won’t update the tooltip. This is a limitation of the library, &lt;a href="https://atomiks.github.io/tippyjs/v6/faq/#changing-data-tippy-attributes-does-not-update-the-tooltip" rel="noopener noreferrer"&gt;documented in the FAQ&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Let’s take a look at the element first, to see what kind of state we’ll need to deal with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The HTML element
&lt;/h2&gt;

&lt;p&gt;Here’s a &lt;code&gt;button&lt;/code&gt; element that will integrate with the hook we’re about to write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"example-button"&lt;/span&gt; &lt;span class="na"&gt;phx-hook=&lt;/span&gt;&lt;span class="s"&gt;"Tippy"&lt;/span&gt; &lt;span class="na"&gt;data-tippy-content=&lt;/span&gt;&lt;span class="s"&gt;"Hello!"&lt;/span&gt; &lt;span class="na"&gt;data-tippy-placement=&lt;/span&gt;&lt;span class="s"&gt;"bottom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Hover over me
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are several things to note here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; is required for all elements that have a &lt;code&gt;phx-hook&lt;/code&gt; associated with them, so Phoenix LiveView can associate internal state by element ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phx-hook&lt;/code&gt; associates a particular hook. Only (at most) one hook is permitted per element&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-tippy-&lt;/code&gt; is the prefix for each attribute. We’ll need to process these &lt;code&gt;dataset&lt;/code&gt; elements (see &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset" rel="noopener noreferrer"&gt;the dataset property&lt;/a&gt; documentation) to pass a configuration object when we come to update the properties associated with the tooltip. In short: &lt;code&gt;data-tippy-placement=”bottom”&lt;/code&gt; in the DOM becomes &lt;code&gt;this.el.dataset.tippyPlacement // =&amp;gt; “bottom”&lt;/code&gt; in Javascript.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The LiveView hook
&lt;/h2&gt;

&lt;p&gt;Here’s the code for the hook. Let’s go through the below section by section.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Hooks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;Tippy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tippy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tippy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
              &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tippyPropName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
              &lt;span class="nx"&gt;v&lt;/span&gt;
            &lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;destroyed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;

    &lt;span class="nf"&gt;tippyPropName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;strippedName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tippy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;strippedName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;strippedName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  mounted()
&lt;/h3&gt;

&lt;p&gt;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):&lt;/p&gt;

&lt;p&gt;this.instance = tippy(this.el);&lt;/p&gt;

&lt;p&gt;After this, tippy.js takes care of attaching to the relevant DOM events and invoking the logic to display the tooltip.&lt;/p&gt;

&lt;h3&gt;
  
  
  updated()
&lt;/h3&gt;

&lt;p&gt;As noted above, tippy.js doesn’t update its internal state if the values of attributes change.&lt;/p&gt;

&lt;p&gt;this.instance.setProps(&lt;br&gt;
    Object.fromEntries(&lt;br&gt;
      Object.entries(this.el.dataset)&lt;br&gt;
        .filter(([k]) =&amp;gt; k.startsWith("tippy"))&lt;br&gt;
        .map(([k, v]) =&amp;gt; [&lt;br&gt;
          this.tippyPropName(k),&lt;br&gt;
          v&lt;br&gt;
        ])&lt;br&gt;
    )&lt;br&gt;
);&lt;/p&gt;

&lt;p&gt;Step by step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It starts with &lt;code&gt;this.el.dataset&lt;/code&gt;, which contains all data attributes of an HTML element.&lt;/li&gt;
&lt;li&gt;It filters these attributes to only include those that start with "tippy" (&lt;code&gt;data-tippy&lt;/code&gt; in the HTML)&lt;/li&gt;
&lt;li&gt;For each filtered attribute, it transforms the attribute name into a valid Tippy.js property name using a tippyPropName method.&lt;/li&gt;
&lt;li&gt;It creates an object from these transformed key-value pairs.&lt;/li&gt;
&lt;li&gt;Finally, it sets these properties on the Tippy.js instance using setProps().&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Take care that the representation within the tooltip’s data attributes is something that is either a string or a JSON representation, depending on the type of the tippy.js attribute.)&lt;/p&gt;

&lt;p&gt;Another alternative: we could invoke &lt;code&gt;tippy&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  destroyed()
&lt;/h3&gt;

&lt;p&gt;Without this, tippy.js tooltip instances accumulate during LiveView updates, causing memory leaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  tippyPropName(k)
&lt;/h3&gt;

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

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tippy.js&lt;/code&gt; is by no means the only tooltip library that can be integrated with Phoenix LiveView:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/ryangjchandler/alpine-tooltip" rel="noopener noreferrer"&gt;alpine-tooltip&lt;/a&gt; is a popular plugin for Alpine.js, commonly used as a simple Javascript dynamic layer in Phoenix LiveView applications&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://floating-ui.com/docs/getting-started" rel="noopener noreferrer"&gt;Floating UI&lt;/a&gt; is a library (superseding popper.js) that helps you create “floating” elements such as tooltips, popovers, dropdowns and more. Floating UI is framework-independent.&lt;/li&gt;
&lt;li&gt;Use Phoenix LiveView component libraries like &lt;a href="https://github.com/bluzky/salad_ui" rel="noopener noreferrer"&gt;salad_ui&lt;/a&gt; or &lt;a href="https://mishka.tools/chelekom" rel="noopener noreferrer"&gt;Chelekom&lt;/a&gt;, which contain their own tooltip component implementations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>liveview</category>
      <category>elixir</category>
      <category>tooltips</category>
    </item>
  </channel>
</rss>
