This walkthrough shows how to set up tooltips in a new Rails+Stimulus app using Tippy.js. This simple from-scratch example should be easy to customize, reuse, etc.
Below, I'm going to walk through the 2 commits in this repo. The first commit sets up a simple Rails 7 app and the second implements a tooltip feature with some customization.
=====
Commit 1: Create New App
-------
Check versions:
Below, I'm going to walk through the 2 commits in this repo. The first commit sets up a simple Rails 7 app and the second implements a tooltip feature with some customization.
=====
Commit 1: Create New App
-------
Check versions:
❯ ruby -v ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-darwin22]
❯ rails -v Rails 7.1.2
❯ rails new rails-stimulus-tooltips -j esbuild -d postgresql
We're using esbuild for JS bundling so we can use Yarn to install and use Tippy.js. You could easily do this with importmaps instead of esbuild, but I still default to esbuild because it offers more flexibility for interacting with JS packages.
After
cd
ing into the new directory, create a database with rails db:create
, run bin/dev
, and navigate to localhost:3000 in your browser. The Rails welcome screen should appear:=====
Commit 2: Tooltip Setup
-------
First let's set up a simple static view. Create a controller at
app/controllers/static_controller.rb
:class StaticController < ApplicationController end
Create a view at
app/views/static/index.html.erb
:<div data-controller="tooltips"> <span id="tooltip-1--trigger">Tooltip 1</span> </div>
The operative parts here are the
data-controller
attribute and the span id of "tooltip-1--trigger." We'll use these to interact with Stimulus to display the tooltips. Point the root route to the
static#index
action in config/routes.rb
(Rails will render the index view by inference, so no need to define the action in the controller):Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") # root "posts#index" root "static#index" end
At this point, refreshing the app in the browser should display the view (interactions won't work):
To install Tippy.js, run
yarn add tippy.js
. You should see something like this:❯ yarn add tippy.js yarn add v1.9.2 [1/4] 🔍 Resolving packages... [2/4] 🚚 Fetching packages... [3/4] 🔗 Linking dependencies... [4/4] 📃 Building fresh packages... success Saved lockfile. success Saved 2 new dependencies. info Direct dependencies └─ tippy.js@6.3.7 info All dependencies ├─ @popperjs/core@2.11.8 └─ tippy.js@6.3.7 ✨ Done in 1.13s.
You should see Tippy.js in
package.json
:"dependencies": { "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.0-beta.2", "esbuild": "^0.19.10", "tippy.js": "^6.3.7" }, "scripts": { "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=/assets"
You will also see Tippy.js and Popperjs added to yarn.lock. Popper is a dependency of Tippy.js. If you are interested in the future of Popper or floating elements in general, it's worth checking out the new Floating UI library that may eventually replace Popper.
The final step is setting up an actual tooltip, which Tippy.js makes very easy.
Start by renaming
app/javascript/controllers/hello_controller.js
to app/javascript/controllers/tooltips_controller.js
and running stimulus:manifest:update
to update the controller imports at app/javascript/controllers/index.js
. index.js
should look like this:import { application } from "./application" import TooltipsController from "./tooltips_controller" application.register("tooltips", TooltipsController)
That brings us to the tooltip code, which I'll copy here and then explain:
import { Controller } from "@hotwired/stimulus"; import tippy from "tippy.js"; export default class extends Controller { connect() { var el = document.getElementById("tooltip-1--trigger") if (el) { const instance = tippy(el, { theme: 'info', hideOnClick: false, trigger: 'click', interactive: true, allowHTML: true, content: ` <div class="tooltip"> \ <div>Here is some info</div> \ <div> \ And <a href="https://www.theverge.com/2013/5/9/4316222/simcity-lead-designer-stone-librande-talks-about-building-game" target="_blank">links work too</a>! \ </div> \ <button class="close-tooltip">Close</button> \ </div> `, onShow(instance) { const closeTooltipButtons = Array.from(instance.popper.querySelectorAll('.close-tooltip')); closeTooltipButtons.forEach(button => { button.addEventListener('click', function () { instance.hide(); }); }); } }) } } }
First we're importing the Tippy.js library. Then, when the controller connects (i.e., when the index view loads), it creates a tooltip for the
tooltip-1--trigger
element with the specified properties and a custom function and theme. We are setting the tooltip be triggered by a click instead of the default, which displays the tooltip on hover. We are setting the tooltip to not close when we click elsewhere on the screen. We are providing some HTML content for the actual tooltip. And, we are setting the tooltip to be interactive and allow HTML inside. These are all non-standard tooltip behaviors, but I added them as a means to show ways you might get creative with using tooltips.
The
onShow
function sets up the mechanics for using our close button to close the modal. I wrote this function assuming there might be multiple tooltips in this controller. YMMV and this part should be customized for your use case/setup. Finally, we are setting a theme called
info
. This is a custom theme, meaning that it doesn't trigger anything in the Tippy library itself (Tippy offers a few pre-baked themes that I won't cover in this walkthrough). Instead, all it does it add a custom data attribute to the tooltip so we can target it with CSS, which I added in app/assets/stylesheets/application.css
:/* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_tree . *= require_self */ #tooltip-1--trigger:hover { cursor: pointer; text-decoration: underline; } .tippy-box[data-theme~='info'] { background-color: #c5c5c5; color: #555555; padding: 5px 8px; border-radius: 2px; } .tippy-box #tooltip-1 { width: 200px; height: 100px; } button.close-tooltip { margin-top: 10px; }
This CSS adds an underline to the tooltip trigger on hover. It also adds styling to
.tippy-box
elements with the data-theme
attribute equal to "info." This is how the custom info
theme is implemented in CSS, super straightforward. I also added some basic style to .tippy-box #tooltip-1, targeting the specific HTML ID that I included in the Stimulus controller. Finally, I added a top margin to the close button to add some visual spacing.When you click on "Tooltip 1," the tooltip will now appear with working link and it will close only when you press the "Close" button (not when you click elsewhere on the screen):
That's all. Very rudimentary example, but easy to customize, refactor, etc. from here.