Tooltip
A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
Usage
Example
Add to library
Tooltip do
TooltipTrigger do
Button(variant: :outline, icon: true) do
bookmark_icon
end
end
TooltipContent do
Text { "Add to library" }
end
end
Long content
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
Tooltip do
TooltipTrigger do
Button(variant: :outline) { "Hover me" }
end
TooltipContent do
Text { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." }
end
end
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component Tooltip
Manual installation
1
Add RubyUI::Tooltip to app/components/ruby_ui/tooltip/tooltip.rb
# frozen_string_literal: true
module RubyUI
class Tooltip < Base
def initialize(placement: "top", **attrs)
@placement = placement
super(**attrs)
end
def view_template(&)
div(**attrs, &)
end
private
def default_attrs
{
data: {
controller: "ruby-ui--tooltip",
ruby_ui__tooltip_placement_value: @placement
},
class: "group"
}
end
end
end
2
Add RubyUI::TooltipContent to app/components/ruby_ui/tooltip/tooltip_content.rb
# frozen_string_literal: true
module RubyUI
class TooltipContent < Base
def initialize(**attrs)
@id = "tooltip#{SecureRandom.hex(4)}"
super
end
def view_template(&)
template(data: {ruby_ui__tooltip_target: "content"}) do
div(**attrs, &)
end
end
private
def default_attrs
{
id: @id,
class: [
"invisible pointer-events-none w-fit max-w-[calc(100vw-2rem)] text-balance break-words absolute top-0 left-0 z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md",
"data-[placement=bottom]:slide-in-from-top-2",
"data-[placement=left]:slide-in-from-right-2",
"data-[placement=right]:slide-in-from-left-2",
"data-[placement=top]:slide-in-from-bottom-2",
"data-[state=open]:visible data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
"data-[state=closed]:visible data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:fill-mode-forwards"
]
}
end
end
end
3
Add RubyUI::TooltipDocs to app/components/ruby_ui/tooltip/tooltip_docs.rb
# frozen_string_literal: true
class Views::Docs::Tooltip < Views::Base
def view_template
component = "Tooltip"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Tooltip", description: "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.")
Heading(level: 2) { "Usage" }
render Docs::VisualCodeExample.new(title: "Example", context: self) do
<<~RUBY
Tooltip do
TooltipTrigger do
Button(variant: :outline, icon: true) do
bookmark_icon
end
end
TooltipContent do
Text { "Add to library" }
end
end
RUBY
end
render Docs::VisualCodeExample.new(title: "Long content", context: self) do
<<~RUBY
Tooltip do
TooltipTrigger do
Button(variant: :outline) { "Hover me" }
end
TooltipContent do
Text { "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." }
end
end
RUBY
end
render Components::ComponentSetup::Tabs.new(component_name: component)
render Docs::ComponentsTable.new(component_files(component))
end
end
private
def bookmark_icon
svg(
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
viewbox: "0 0 24 24",
stroke_width: "1.5",
stroke: "currentColor",
class: "w-4 h-4"
) do |s|
s.path(
stroke_linecap: "round",
stroke_linejoin: "round",
d:
"M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0111.186 0z"
)
end
end
end
4
Add RubyUI::TooltipTrigger to app/components/ruby_ui/tooltip/tooltip_trigger.rb
# frozen_string_literal: true
module RubyUI
class TooltipTrigger < Base
def view_template(&)
div(**attrs, &)
end
private
def default_attrs
{
data: {
ruby_ui__tooltip_target: "trigger",
action: [
"mouseenter->ruby-ui--tooltip#show",
"mouseleave->ruby-ui--tooltip#hide",
"focus->ruby-ui--tooltip#show",
"blur->ruby-ui--tooltip#hide"
]
},
variant: :outline
}
end
end
end
5
Add tooltip_controller.js to app/javascript/controllers/ruby_ui/tooltip_controller.js
import { Controller } from "@hotwired/stimulus";
import { computePosition, autoUpdate, offset, shift } from "@floating-ui/dom";
export default class extends Controller {
static targets = ["trigger", "content"];
static values = { placement: "top" };
mount() {
if (this.mounted) return;
const element = this.cloneTemplate();
element.setAttribute("data-placement", this.placementValue);
document.body.appendChild(element);
this.triggerTarget.setAttribute("aria-describedby", element.id);
// animationcancel covers an exit run cut short (backgrounded tab, interrupting
// style change) — without it the cloned node is never removed from <body>.
element.addEventListener("animationend", this.handleExitAnimationEnd);
element.addEventListener("animationcancel", this.handleExitAnimationEnd);
const onBeforeCache = () => this.unmount();
document.addEventListener("turbo:before-cache", onBeforeCache);
this.mounted = { element, onBeforeCache };
this.mounted.stopAutoUpdate = autoUpdate(this.triggerTarget, element, () => this.reposition());
}
unmount() {
if (!this.mounted) return;
document.removeEventListener("turbo:before-cache", this.mounted.onBeforeCache);
this.mounted.stopAutoUpdate?.();
this.mounted.element.remove();
this.triggerTarget.removeAttribute("aria-describedby");
this.mounted = null;
}
disconnect() {
this.unmount();
}
show() {
if (!this.hasContentTarget) return;
this.mount();
this.mounted.element.setAttribute("data-state", "open");
}
hide() {
this.mounted?.element.setAttribute("data-state", "closed");
}
handleExitAnimationEnd = (event) => {
if (event.animationName !== "exit") return;
if (this.mounted?.element.getAttribute("data-state") !== "closed") return;
this.unmount();
};
cloneTemplate() {
return this.contentTarget.content.firstElementChild.cloneNode(true);
}
reposition() {
if (!this.mounted) return;
const position = { placement: this.placementValue, middleware: [offset(4), shift()] };
computePosition(this.triggerTarget, this.mounted.element, position).then(({ x, y }) => {
this.mounted?.element.style.setProperty("left", `${x}px`);
this.mounted?.element.style.setProperty("top", `${y}px`);
});
}
}
6
Update the Stimulus controllers manifest file
rake stimulus:manifest:update7
Install @floating-ui/dom Javascript dependency
// with yarn
yarn add @floating-ui/dom
// with npm
npm install @floating-ui/dom
// with importmaps
bin/importmap pin @floating-ui/dom
8
Install required components
Component Tooltip relies on the following RubyUI components. Refer to their individual installation guides for setup instructions:
Components
| Component | Built using | Source |
|---|---|---|
Tooltip | Phlex | |
TooltipContent | Phlex | |
TooltipDocs | Phlex | |
TooltipTrigger | Phlex | |
TooltipController | Stimulus JS |