Tooltip Rails Components
Display helpful hints, labels, and contextual information on hover or focus. Built with Floating UI for intelligent positioning and smooth animations.
Installation
1. Stimulus Controller Setup
Start by adding the following controller to your project:
import { Controller } from "@hotwired/stimulus";
import { computePosition, offset, flip, shift, arrow, autoUpdate } from "@floating-ui/dom";
// Global tooltip state manager for intelligent behavior
class TooltipGlobalState {
constructor() {
this.visibleCount = 0;
this.isFastMode = false;
this.resetTimeout = null;
this.fastModeResetDelay = 100; // 0.1 seconds
this.visibleTooltips = new Set(); // Track currently visible tooltip controllers
}
// Called when a tooltip becomes visible
onTooltipShow(tooltipController) {
this.visibleTooltips.add(tooltipController);
this.visibleCount = this.visibleTooltips.size;
if (this.visibleCount > 0 && !this.isFastMode) {
this.isFastMode = true;
}
this.clearResetTimeout();
}
// Called when a tooltip is hidden
onTooltipHide(tooltipController) {
this.visibleTooltips.delete(tooltipController);
this.visibleCount = this.visibleTooltips.size;
// If no tooltips are visible, start countdown to exit fast mode
if (this.visibleCount === 0) {
this.startResetTimeout();
}
}
// Hide all currently visible tooltips instantly (for fast mode switching)
hideAllVisibleTooltipsInstantly(exceptController) {
const tooltipsToHide = [...this.visibleTooltips].filter((controller) => controller !== exceptController);
tooltipsToHide.forEach((controller) => {
controller._hideTooltip(true); // true = instant hide
});
}
// Check if we're in fast mode
isInFastMode() {
return this.isFastMode;
}
// Start timeout to reset fast mode
startResetTimeout() {
this.clearResetTimeout();
this.resetTimeout = setTimeout(() => {
this.isFastMode = false;
}, this.fastModeResetDelay);
}
// Clear the reset timeout
clearResetTimeout() {
if (this.resetTimeout) {
clearTimeout(this.resetTimeout);
this.resetTimeout = null;
}
}
}
// Global instance
const tooltipGlobalState = new TooltipGlobalState();
export default class extends Controller {
// placement and offset can still be configured via data-tooltip-placement-value etc. if desired,
// but will use defaults if the original HTML (using data-tooltip-*) doesn't provide them.
static values = {
placement: { type: String, default: "top" }, // Placement(s) of the tooltip, e.g., "top", "top-start", "top-end", "bottom", "bottom-start", "bottom-end", "left", "left-start", "left-end", "right", "right-start", "right-end"
offset: { type: Number, default: 8 }, // Offset of the tooltip
maxWidth: { type: Number, default: 200 }, // Default max width for tooltips
delay: { type: Number, default: 0 }, // Delay before showing the tooltip (in ms)
size: { type: String, default: "regular" }, // Size of the tooltip, e.g., "small", "regular", "large"
animation: { type: String, default: "fade" }, // e.g., "fade", "origin", "fade origin", "none"
trigger: { type: String, default: "mouseenter focus" }, // space-separated: mouseenter, focus
// tooltipContent and tooltipArrow are read directly from element attributes in connect()
};
_hasAnimationType(type) {
return this.animationValue.split(" ").includes(type);
}
connect() {
this.tooltipContent = this.element.getAttribute("data-tooltip-content") || "";
this.showArrow = this.element.getAttribute("data-tooltip-arrow") !== "false";
this.showTimeoutId = null;
this.hideTimeoutId = null;
this.isVisible = false;
if (!this.tooltipContent) {
console.warn("Tooltip initialized without data-tooltip-content", this.element);
return;
}
this.tooltipElement = document.createElement("div");
this.tooltipElement.className =
"tooltip-content pointer-events-none wrap-break-word shadow-sm border rounded-lg border-white/10 absolute bg-[#333333] text-white py-1 px-2 z-[1000]";
const sizeClasses = {
small: "text-xs",
regular: "text-sm",
large: "text-base",
};
const sizeClass = sizeClasses[this.sizeValue] || sizeClasses.regular;
this.tooltipElement.classList.add(sizeClass);
// Always start transparent and hidden. Visibility/opacity managed by show/hide logic.
this.tooltipElement.classList.add("opacity-0");
this.tooltipElement.style.visibility = "hidden";
// Base transition for all animations that might use opacity or transform
if (this._hasAnimationType("fade") || this._hasAnimationType("origin")) {
this.tooltipElement.classList.add("transition-all"); // Use transition-all for simplicity if combining
}
if (this._hasAnimationType("fade")) {
// Ensure specific duration for opacity if not covered by a general one or if different
this.tooltipElement.classList.add("duration-150"); // Default fade duration
}
if (this._hasAnimationType("origin")) {
// Ensure specific duration for transform if not covered by a general one or if different
this.tooltipElement.classList.add("duration-150", "ease-out"); // Default origin duration and ease
this.tooltipElement.classList.add("scale-95"); // Initial state for origin animation
}
this.tooltipElement.innerHTML = this.tooltipContent;
this.tooltipElement.style.maxWidth = `${this.maxWidthValue}px`;
if (this.showArrow) {
// Create arrow container with padding to prevent clipping at viewport edges
this.arrowContainer = document.createElement("div");
this.arrowContainer.className = "absolute z-[1000]";
// Create the arrow element within the container
this.arrowElement = document.createElement("div");
this.arrowElement.className = "tooltip-arrow-element bg-[#333333] w-2 h-2 border-white/10";
this.arrowElement.style.transform = "rotate(45deg)";
this.arrowContainer.appendChild(this.arrowElement);
this.tooltipElement.appendChild(this.arrowContainer);
}
// Append target logic is handled in _showTooltip to ensure it's correct at showtime
// const appendTarget = this.element.closest("dialog[open]") || document.body;
// appendTarget.appendChild(this.tooltipElement);
this.showTooltipBound = this._showTooltip.bind(this);
this.hideTooltipBound = this._hideTooltip.bind(this);
this.clickHideTooltipBound = this._handleClick.bind(this);
const triggers = this.triggerValue.split(" ");
this.hasMouseEnterTrigger = triggers.includes("mouseenter");
triggers.forEach((event_type) => {
if (event_type === "mouseenter") {
this.element.addEventListener("mouseenter", this.showTooltipBound);
this.element.addEventListener("mouseleave", this.hideTooltipBound);
}
if (event_type === "focus") {
this.element.addEventListener("focus", this.showTooltipBound);
this.element.addEventListener("blur", this.hideTooltipBound);
}
});
// Add click event to close tooltip but allow event to bubble up
if (this.hasMouseEnterTrigger) {
this.element.addEventListener("click", this.clickHideTooltipBound);
}
this.cleanupAutoUpdate = null;
this.intersectionObserver = null;
}
disconnect() {
clearTimeout(this.showTimeoutId);
clearTimeout(this.hideTimeoutId); // Clear hideTimeoutId on disconnect
// Remove from global state if visible
if (this.isVisible) {
tooltipGlobalState.onTooltipHide(this);
this.isVisible = false;
}
this.triggerValue.split(" ").forEach((event_type) => {
if (event_type === "mouseenter") {
this.element.removeEventListener("mouseenter", this.showTooltipBound);
this.element.removeEventListener("mouseleave", this.hideTooltipBound);
}
if (event_type === "focus") {
this.element.removeEventListener("focus", this.showTooltipBound);
this.element.removeEventListener("blur", this.hideTooltipBound);
}
});
// Remove click event listener only if it was added for hover-enabled tooltips
if (this.hasMouseEnterTrigger) {
this.element.removeEventListener("click", this.clickHideTooltipBound);
}
if (this.cleanupAutoUpdate) {
this.cleanupAutoUpdate();
this.cleanupAutoUpdate = null;
}
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
if (this.tooltipElement && this.tooltipElement.parentElement) {
this.tooltipElement.remove();
}
if (this.arrowElement) {
// Arrow is child of tooltipElement, removed with it.
}
}
async _updatePositionAndArrow() {
if (!this.element || !this.tooltipElement) return;
// Parse placement value to support multiple placements
const placements = this.placementValue.split(/[\s,]+/).filter(Boolean);
const primaryPlacement = placements[0] || "top";
const fallbackPlacements = placements.slice(1);
const middleware = [
offset(this.offsetValue),
flip({
fallbackPlacements: fallbackPlacements.length > 0 ? fallbackPlacements : undefined,
}),
shift({ padding: 5 }),
];
if (this.showArrow && this.arrowContainer) {
middleware.push(arrow({ element: this.arrowContainer, padding: 2 }));
}
const { x, y, placement, middlewareData } = await computePosition(this.element, this.tooltipElement, {
placement: primaryPlacement,
middleware: middleware,
});
Object.assign(this.tooltipElement.style, {
left: `${x}px`,
top: `${y}px`,
});
if (this._hasAnimationType("origin")) {
const basePlacement = placement.split("-")[0];
this.tooltipElement.classList.remove("origin-top", "origin-bottom", "origin-left", "origin-right");
if (basePlacement === "top") {
this.tooltipElement.classList.add("origin-bottom");
} else if (basePlacement === "bottom") {
this.tooltipElement.classList.add("origin-top");
} else if (basePlacement === "left") {
this.tooltipElement.classList.add("origin-right");
} else if (basePlacement === "right") {
this.tooltipElement.classList.add("origin-left");
}
}
if (this.showArrow && this.arrowContainer && this.arrowElement && middlewareData.arrow) {
const { x: arrowX, y: arrowY } = middlewareData.arrow;
const currentPlacement = placement; // Use the resolved placement from computePosition
const basePlacement = currentPlacement.split("-")[0];
const staticSide = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
}[basePlacement];
// Apply appropriate padding based on placement direction
this.arrowContainer.classList.remove("px-1", "py-1");
if (basePlacement === "top" || basePlacement === "bottom") {
this.arrowContainer.classList.add("px-1"); // Horizontal padding for top/bottom
} else {
this.arrowContainer.classList.add("py-1"); // Vertical padding for left/right
}
// Position the arrow container
Object.assign(this.arrowContainer.style, {
left: arrowX != null ? `${arrowX}px` : "",
top: arrowY != null ? `${arrowY}px` : "",
right: "",
bottom: "",
[staticSide]: "-0.275rem", // Adjusted to -0.275rem as often seen with 0.5rem arrows
});
// Style the arrow element within the container
// Reset existing border classes before adding new ones
this.arrowElement.classList.remove("border-t", "border-r", "border-b", "border-l");
// Apply new borders based on placement
if (staticSide === "bottom") {
// Arrow points up
this.arrowElement.classList.add("border-b", "border-r");
} else if (staticSide === "top") {
// Arrow points down
this.arrowElement.classList.add("border-t", "border-l");
} else if (staticSide === "left") {
// Arrow points right
this.arrowElement.classList.add("border-b", "border-l");
} else if (staticSide === "right") {
// Arrow points left
this.arrowElement.classList.add("border-t", "border-r");
}
}
}
async _showTooltip() {
if (!this.tooltipElement) return;
clearTimeout(this.hideTimeoutId); // Cancel any pending hide finalization
clearTimeout(this.showTimeoutId); // Cancel any pending show
// Determine if we should use fast mode (no delay, no animations)
const isFastMode = tooltipGlobalState.isInFastMode();
const effectiveDelay = isFastMode ? 0 : this.delayValue;
this.showTimeoutId = setTimeout(async () => {
// Always hide all other visible tooltips instantly when showing a new one
// This ensures only one tooltip is visible at a time with smooth transitions
tooltipGlobalState.hideAllVisibleTooltipsInstantly(this);
// Ensure tooltip is appended to the correct target (body or open dialog)
// This is done here to handle cases where the element might move into/out of a dialog
const currentAppendTarget = this.element.closest("dialog[open]") || document.body;
if (this.tooltipElement.parentElement !== currentAppendTarget) {
currentAppendTarget.appendChild(this.tooltipElement);
}
// Tooltip is already opacity-0 and visibility-hidden from connect()
// 1. Calculate and apply position
await this._updatePositionAndArrow();
// 2. Make it visible
this.tooltipElement.style.visibility = "visible";
// 3. Apply opacity and scale based on animation type
// In fast mode, temporarily disable transitions for instant changes
if (isFastMode) {
// Temporarily disable all transitions
this.tooltipElement.style.setProperty("transition", "none", "important");
// Force a reflow to ensure the transition: none is applied
this.tooltipElement.offsetHeight;
}
const applyChanges = () => {
let applyOpacity100 = false;
let applyScale100 = false;
if (isFastMode) {
// In fast mode, always apply full opacity and scale immediately
applyOpacity100 = true;
if (this._hasAnimationType("origin")) {
applyScale100 = true;
}
} else {
// Normal mode - use configured animations
if (this._hasAnimationType("fade")) {
applyOpacity100 = true;
}
if (this._hasAnimationType("origin")) {
applyOpacity100 = true; // Origin animation also fades in
applyScale100 = true;
}
if (!this._hasAnimationType("fade") && !this._hasAnimationType("origin") && this.animationValue !== "none") {
// Default behavior if animationValue is something unexpected but not 'none': make visible
applyOpacity100 = true;
}
if (this.animationValue === "none") {
applyOpacity100 = true; // No transition, just make it full opacity
}
}
if (applyOpacity100) {
this.tooltipElement.classList.remove("opacity-0");
this.tooltipElement.classList.add("opacity-100");
}
if (applyScale100) {
this.tooltipElement.classList.remove("scale-95");
this.tooltipElement.classList.add("scale-100");
}
// Re-enable transitions after fast mode changes
if (isFastMode) {
// Use requestAnimationFrame to ensure the styles are applied first
requestAnimationFrame(() => {
if (this.tooltipElement) {
this.tooltipElement.style.removeProperty("transition");
}
});
}
};
if (isFastMode) {
// Apply changes immediately in fast mode
applyChanges();
} else {
// Use requestAnimationFrame for smooth animations in normal mode
requestAnimationFrame(applyChanges);
}
// 4. Setup autoUpdate for continuous positioning
if (this.cleanupAutoUpdate) {
this.cleanupAutoUpdate();
}
this.cleanupAutoUpdate = autoUpdate(
this.element,
this.tooltipElement,
async () => {
// Re-check append target in case DOM changes during interaction
const appendTargetRecurring = this.element.closest("dialog[open]") || document.body;
if (this.tooltipElement.parentElement !== appendTargetRecurring) {
appendTargetRecurring.appendChild(this.tooltipElement);
}
await this._updatePositionAndArrow();
},
{ animationFrame: true } // Use animationFrame for smoother updates
);
// 5. Setup intersection observer to hide tooltip when trigger element goes out of view
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
this.intersectionObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) {
this._hideTooltip();
}
});
},
{ threshold: 0 } // Hide as soon as any part goes out of view
);
this.intersectionObserver.observe(this.element);
// 6. Register with global state that this tooltip is now visible
if (!this.isVisible) {
this.isVisible = true;
tooltipGlobalState.onTooltipShow(this);
}
}, effectiveDelay);
}
_handleClick() {
// Hide the tooltip but allow the event to bubble up
this._hideTooltip();
// Don't call event.preventDefault() or event.stopPropagation()
// so the event can bubble up to parent elements (like kanban cards)
}
_hideTooltip(isInstantHide = false) {
clearTimeout(this.showTimeoutId); // Cancel any pending show operation
clearTimeout(this.hideTimeoutId); // Cancel any pending hide finalization
if (!this.tooltipElement) return;
// Register with global state that this tooltip is now hidden
if (this.isVisible) {
this.isVisible = false;
tooltipGlobalState.onTooltipHide(this);
}
// Skip animations when instantly hiding (switching to another tooltip)
const shouldSkipAnimations = isInstantHide;
// Start the hide animation by applying target opacity/transform
let targetOpacity0 = false;
let targetScale95 = false;
if (shouldSkipAnimations) {
// In fast mode instant hide, disable transitions first
this.tooltipElement.style.setProperty("transition", "none", "important");
// Force a reflow to ensure the transition: none is applied
this.tooltipElement.offsetHeight;
// Now apply all visual changes instantly
this.tooltipElement.classList.remove("opacity-100");
this.tooltipElement.classList.add("opacity-0");
if (this._hasAnimationType("origin")) {
this.tooltipElement.classList.remove("scale-100");
this.tooltipElement.classList.add("scale-95");
}
this.tooltipElement.style.visibility = "hidden";
// Re-enable transitions after changes are applied
requestAnimationFrame(() => {
if (this.tooltipElement) {
this.tooltipElement.style.removeProperty("transition");
}
});
} else {
// Normal hide with animations
if (this._hasAnimationType("fade")) {
targetOpacity0 = true;
}
if (this._hasAnimationType("origin")) {
targetOpacity0 = true; // Origin animation also fades out
targetScale95 = true;
}
if (this.animationValue === "none" || (!targetOpacity0 && !targetScale95)) {
// If "none" or no animations specified that affect opacity/scale for hiding, set directly
this.tooltipElement.classList.remove("opacity-100");
this.tooltipElement.classList.add("opacity-0");
if (this.animationValue === "none") this.tooltipElement.style.visibility = "hidden";
}
if (targetOpacity0) {
this.tooltipElement.classList.remove("opacity-100");
this.tooltipElement.classList.add("opacity-0");
}
if (targetScale95) {
this.tooltipElement.classList.remove("scale-100");
this.tooltipElement.classList.add("scale-95");
}
}
// Stop auto-updating position
if (this.cleanupAutoUpdate) {
this.cleanupAutoUpdate();
this.cleanupAutoUpdate = null;
}
// Stop intersection observer
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
this.intersectionObserver = null;
}
// Set a timeout for final cleanup (e.g., setting visibility: hidden).
let applicableHideDelay = 0;
if (!shouldSkipAnimations) {
// Only add delay for animations if we're not in fast instant hide mode
if (this._hasAnimationType("fade")) {
applicableHideDelay = Math.max(applicableHideDelay, 150); // Fade duration
}
if (this._hasAnimationType("origin")) {
applicableHideDelay = Math.max(applicableHideDelay, 150); // Origin duration
}
}
// If shouldSkipAnimations is true or animation is "none", applicableHideDelay remains 0
this.hideTimeoutId = setTimeout(() => {
if (this.tooltipElement) {
// Only hide visibility if there was an animation to wait for.
// For instant hides or "none", visibility is handled immediately above.
if (!shouldSkipAnimations && this.animationValue !== "none") {
this.tooltipElement.style.visibility = "hidden";
}
// Ensure final state for opacity and scale, even if transitions didn't run or were interrupted.
this.tooltipElement.classList.remove("opacity-100");
this.tooltipElement.classList.add("opacity-0");
if (this._hasAnimationType("origin")) {
this.tooltipElement.classList.remove("scale-100");
this.tooltipElement.classList.add("scale-95");
}
}
}, applicableHideDelay);
}
}
2. Floating UI Installation
The tooltip component relies on Floating UI for intelligent positioning. Choose your preferred installation method:
pin "@floating-ui/dom", to: "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.7.3/+esm"
npm install @floating-ui/dom
yarn add @floating-ui/dom
Examples
Basic tooltip
Simple tooltips with different placements and arrow options.
<%# Basic tooltip examples %>
<div class="flex flex-col items-center gap-4">
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="This is a simple tooltip">
Hover for Tooltip
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="No arrow on this tooltip"
data-tooltip-arrow="false">
No Arrow
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="This tooltip has a longer text content to show text wrapping within the configured maximum width"
data-tooltip-max-width-value="250">
Long Content
</button>
<%# Help icon example %>
<button type="button"
data-controller="tooltip"
data-tooltip-content="Learn more about a feature"
class="cursor-help shrink-0 inline-flex items-center justify-center size-5 rounded-full border border-neutral-200 bg-neutral-50 text-sm font-semibold leading-5 text-neutral-800 hover:text-neutral-900 dark:border-neutral-600 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:text-neutral-200 focus:outline-offset-2 focus:outline-neutral-500 dark:focus:outline-neutral-200">
<span class="text-xs">?</span>
</button>
<%# Icon button example with bottom placement %>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 p-2.5 text-xs font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Settings"
data-tooltip-placement-value="bottom">
<svg viewBox="0 0 16 16" fill="currentColor" class="size-4"><path d="M8 2a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM8 6.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM9.5 12.5a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0Z"></path></svg>
</button>
<%# Icon button example with left placement %>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 p-2.5 text-xs font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Download file"
data-tooltip-placement-value="left">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="24" height="24" viewBox="0 0 24 24"><g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><path d="M7 10L12 15 17 10"></path><path d="M12 15L12 3"></path></g></svg>
</button>
</div>
Tooltip positions
All 12 available placement options provided by Floating UI.
<%# All 12 tooltip placement positions %>
<div class="flex flex-col sm:grid grid-cols-3 gap-4 place-items-center py-8">
<%# Top row - top positions %>
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="top-start"
data-tooltip-placement-value="top-start">
top-start
</button>
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="top"
data-tooltip-placement-value="top">
top
</button>
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="top-end"
data-tooltip-placement-value="top-end">
top-end
</button>
<%# Middle row - left positions %>
<div class="w-full col-start-1 flex gap-4">
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="left-start"
data-tooltip-placement-value="left-start">
left-start
</button>
</div>
<%# Center - demonstrates the reference point %>
<div class="relative hidden sm:block">
</div>
<%# Middle row - right positions %>
<div class="w-full col-start-3 flex gap-4">
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="right-start"
data-tooltip-placement-value="right-start">
right-start
</button>
</div>
<%# Second middle row %>
<div class="w-full col-start-1 flex gap-4">
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="left"
data-tooltip-placement-value="left">
left
</button>
</div>
<div class="hidden sm:block"></div>
<div class="w-full col-start-3 flex gap-4">
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="right"
data-tooltip-placement-value="right">
right
</button>
</div>
<%# Third middle row %>
<div class="w-full col-start-1 flex gap-4">
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="left-end"
data-tooltip-placement-value="left-end">
left-end
</button>
</div>
<div class="hidden sm:block"></div>
<div class="w-full col-start-3 flex gap-4">
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="right-end"
data-tooltip-placement-value="right-end">
right-end
</button>
</div>
<%# Bottom row - bottom positions %>
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="bottom-start"
data-tooltip-placement-value="bottom-start">
bottom-start
</button>
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="bottom"
data-tooltip-placement-value="bottom">
bottom
</button>
<button class="w-full flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="bottom-end"
data-tooltip-placement-value="bottom-end">
bottom-end
</button>
</div>
Tooltip sizes
Different text sizes for various use cases.
<%# Different tooltip sizes %>
<div class="flex flex-col items-center gap-4">
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Small tooltip text (text-xs)"
data-tooltip-placement-value="top"
data-tooltip-size-value="small">
Small Size
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Regular tooltip text (text-sm)"
data-tooltip-placement-value="top"
data-tooltip-size-value="regular">
Regular Size (Default)
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Large tooltip text (text-base)"
data-tooltip-placement-value="top"
data-tooltip-size-value="large">
Large Size
</button>
<%# With longer content to show differences %>
<div class="mt-6 flex flex-col items-center gap-4">
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="This is a small tooltip with longer content to demonstrate how text wrapping works at different sizes. The small size uses text-xs."
data-tooltip-placement-value="bottom"
data-tooltip-size-value="small"
data-tooltip-max-width-value="300">
Small (Long Content)
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="This is a regular tooltip with longer content to demonstrate how text wrapping works at different sizes. The regular size uses text-sm."
data-tooltip-placement-value="bottom"
data-tooltip-size-value="regular"
data-tooltip-max-width-value="300">
Regular (Long Content)
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="This is a large tooltip with longer content to demonstrate how text wrapping works at different sizes. The large size uses text-base."
data-tooltip-placement-value="bottom"
data-tooltip-size-value="large"
data-tooltip-max-width-value="300">
Large (Long Content)
</button>
</div>
</div>
Tooltip animations
Various animation options including fade, origin, and combined effects.
<%# Different tooltip animation types %>
<div class="flex flex-col items-center gap-4">
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Fade animation (default)"
data-tooltip-placement-value="top"
data-tooltip-animation-value="fade">
Fade Animation
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Origin animation - scales from placement direction"
data-tooltip-placement-value="top"
data-tooltip-animation-value="origin">
Origin Animation
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Combined fade and origin animation"
data-tooltip-placement-value="top"
data-tooltip-animation-value="fade origin">
Combined Animation
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="No animation - instant appearance"
data-tooltip-placement-value="top"
data-tooltip-animation-value="none">
No Animation
</button>
</div>
Delayed tooltips
Tooltips with hover delay to prevent accidental triggers. Once a tooltip is open, hovering over other tooltips quickly will immediately open them with no delay.
Delays are useful in dense UIs to prevent tooltip spam:
<%# Tooltips with different delays %>
<div class="flex flex-col items-center gap-4">
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="No delay (immediate)"
data-tooltip-placement-value="top"
data-tooltip-delay-value="0">
No Delay
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="300ms delay"
data-tooltip-placement-value="top"
data-tooltip-delay-value="300">
300ms Delay
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="500ms delay"
data-tooltip-placement-value="top"
data-tooltip-delay-value="500">
500ms Delay
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="1 second delay"
data-tooltip-placement-value="top"
data-tooltip-delay-value="1000">
1s Delay
</button>
<%# Dense UI example showing benefit of delays %>
<div class="mt-8 flex flex-col items-center gap-2">
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Delays are useful in dense UIs to prevent tooltip spam:</p>
<div class="inline-flex items-center bg-neutral-100 dark:bg-neutral-800 rounded-lg p-1">
<button class="inline-flex items-center justify-center rounded-md p-1.5 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200 dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-animation-value="none"
data-tooltip-content="Bold"
data-tooltip-placement-value="bottom"
data-tooltip-delay-value="500">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
<path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
</svg>
</button>
<button class="inline-flex items-center justify-center rounded-md p-1.5 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200 dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-animation-value="none"
data-tooltip-content="Italic"
data-tooltip-placement-value="bottom"
data-tooltip-delay-value="500">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="4" x2="10" y2="4"></line>
<line x1="14" y1="20" x2="5" y2="20"></line>
<line x1="15" y1="4" x2="9" y2="20"></line>
</svg>
</button>
<button class="inline-flex items-center justify-center rounded-md p-1.5 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200 dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-animation-value="none"
data-tooltip-content="Underline"
data-tooltip-placement-value="bottom"
data-tooltip-delay-value="500">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"></path>
<line x1="4" y1="21" x2="20" y2="21"></line>
</svg>
</button>
<div class="w-px h-5 bg-neutral-300 dark:bg-neutral-600 mx-1"></div>
<button class="inline-flex items-center justify-center rounded-md p-1.5 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200 dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-animation-value="none"
data-tooltip-content="Link"
data-tooltip-placement-value="bottom"
data-tooltip-delay-value="500">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
</svg>
</button>
<button class="inline-flex items-center justify-center rounded-md p-1.5 text-neutral-600 hover:text-neutral-900 hover:bg-neutral-200 dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-animation-value="none"
data-tooltip-content="Quote"
data-tooltip-placement-value="bottom"
data-tooltip-delay-value="500">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.756-2.017-2-2H4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2 1 0 1 0 1 1v1c0 1-1 2-2 2s-1 .008-1 1.031V20c0 1 0 1 1 1z"></path>
<path d="M15 21c3 0 7-1 7-8V5c0-1.25-.757-2.017-2-2h-4c-1.25 0-2 .75-2 1.972V11c0 1.25.75 2 2 2h.75c0 2.25.25 4-2.75 4v3c0 1 0 1 1 1z"></path>
</svg>
</button>
</div>
</div>
</div>
Trigger options
Control when tooltips appear with different trigger events.
Focus triggers are useful for form elements:
<%# Different tooltip trigger events %>
<div class="flex flex-col items-center gap-4">
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Hover only - tooltip appears on mouse hover"
data-tooltip-placement-value="top"
data-tooltip-trigger-value="mouseenter">
Hover Only
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Focus only - tooltip appears on keyboard focus (Tab to me!)"
data-tooltip-placement-value="top"
data-tooltip-trigger-value="focus">
Focus Only
</button>
<button class="flex items-center justify-center gap-1.5 rounded-lg border border-neutral-200 bg-white/90 px-3.5 py-2 text-sm font-medium whitespace-nowrap text-neutral-800 shadow-xs transition-all duration-100 ease-in-out select-none hover:bg-neutral-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-neutral-600 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-50 dark:hover:bg-neutral-700/50 dark:focus-visible:outline-neutral-200"
data-controller="tooltip"
data-tooltip-content="Both hover and focus triggers (default)"
data-tooltip-placement-value="top"
data-tooltip-trigger-value="mouseenter focus">
Hover & Focus (Default)
</button>
<%# Form elements with focus triggers %>
<div class="mt-8 space-y-4">
<p class="text-sm text-neutral-500 dark:text-neutral-400">Focus triggers are useful for form elements:</p>
<div class="space-y-3 max-w-sm">
<div>
<input type="text"
placeholder="Username"
class="form-control"
data-controller="tooltip"
data-tooltip-content="Your username must be unique and contain only letters, numbers, and underscores"
data-tooltip-placement-value="right bottom"
data-tooltip-trigger-value="focus"
data-tooltip-max-width-value="250">
</div>
<div>
<input type="email"
placeholder="Email address"
class="form-control"
data-controller="tooltip"
data-tooltip-content="We'll use this for account recovery and important notifications"
data-tooltip-placement-value="right bottom"
data-tooltip-trigger-value="focus"
data-tooltip-max-width-value="250">
</div>
<div>
<textarea placeholder="Tell us about yourself..."
rows="3"
class="form-control min-h-24 max-h-48"
data-controller="tooltip"
data-tooltip-content="Write a brief description (max 500 characters)"
data-tooltip-placement-value="right bottom"
data-tooltip-trigger-value="focus"></textarea>
</div>
</div>
</div>
</div>
Advanced tooltip examples
Complex tooltip configurations and real-world use cases.
Status indicators with descriptive tooltips:
Truncated text with full content in tooltip:
Data visualization with informative tooltips:
Table actions with tooltips:
File | Size | Actions |
---|---|---|
document.pdf | 2.4 MB |
|
<%# Advanced tooltip configurations and real-world examples %>
<div class="flex flex-col items-center gap-4">
<%# Status indicators with tooltips %>
<div>
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Status indicators with descriptive tooltips:</p>
<div class="flex items-center gap-4">
<div class="flex items-center gap-2"
data-controller="tooltip"
data-tooltip-content="All systems operational"
data-tooltip-placement-value="top">
<div class="size-2 rounded-full bg-green-500 animate-pulse"></div>
<span class="text-sm">Online</span>
</div>
<div class="flex items-center gap-2"
data-controller="tooltip"
data-tooltip-content="Scheduled maintenance in progress"
data-tooltip-placement-value="top">
<div class="size-2 rounded-full bg-yellow-500"></div>
<span class="text-sm">Maintenance</span>
</div>
<div class="flex items-center gap-2"
data-controller="tooltip"
data-tooltip-content="Service temporarily unavailable"
data-tooltip-placement-value="top">
<div class="size-2 rounded-full bg-red-500"></div>
<span class="text-sm">Offline</span>
</div>
</div>
</div>
<%# Truncated text with tooltip showing full content %>
<div class="flex flex-col items-center">
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Truncated text with full content in tooltip:</p>
<div class="max-w-xs space-y-2">
<div class="truncate text-sm bg-neutral-100 dark:bg-neutral-800 px-3 py-2 rounded"
data-controller="tooltip"
data-tooltip-content="This is a very long filename that would normally be truncated in the UI but can be viewed in full using this tooltip."
data-tooltip-placement-value="top"
data-tooltip-max-width-value="300">
This is a very long filename that would normally be truncated in the UI but can be viewed in full using this tooltip.
</div>
<div class="truncate text-sm bg-neutral-100 dark:bg-neutral-800 px-3 py-2 rounded"
data-controller="tooltip"
data-tooltip-content="/Users/johndoe/Documents/Projects/2024/ClientWork/AcmeCorporation/Proposals/Q4/Final/approved_proposal_v3_final_final.docx"
data-tooltip-placement-value="top"
data-tooltip-max-width-value="300">
/Users/johndoe/Documents/Projects/2024/ClientWork/AcmeCorporation/Proposals/Q4/Final/approved_proposal_v3_final_final.docx
</div>
</div>
</div>
<%# Data visualization with tooltips %>
<div class="flex flex-col items-center">
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Data visualization with informative tooltips:</p>
<div class="flex items-end gap-2 h-32 border border-neutral-200 dark:border-neutral-700 rounded-lg p-2">
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 40%"
data-controller="tooltip"
data-tooltip-content="Monday: 245 visitors"
data-tooltip-placement-value="top"></div>
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 65%"
data-controller="tooltip"
data-tooltip-content="Tuesday: 398 visitors"
data-tooltip-placement-value="top"></div>
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 80%"
data-controller="tooltip"
data-tooltip-content="Wednesday: 489 visitors"
data-tooltip-placement-value="top"></div>
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 55%"
data-controller="tooltip"
data-tooltip-content="Thursday: 336 visitors"
data-tooltip-placement-value="top"></div>
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 90%"
data-controller="tooltip"
data-tooltip-content="Friday: 550 visitors (Peak)"
data-tooltip-placement-value="top"></div>
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 45%"
data-controller="tooltip"
data-tooltip-content="Saturday: 275 visitors"
data-tooltip-placement-value="top"></div>
<div class="w-4 bg-blue-500 rounded-t cursor-pointer hover:bg-blue-600 transition-colors"
style="height: 30%"
data-controller="tooltip"
data-tooltip-content="Sunday: 183 visitors"
data-tooltip-placement-value="top"></div>
</div>
</div>
<%# Table with action buttons and tooltips %>
<div class="flex flex-col items-center">
<p class="text-sm text-neutral-500 dark:text-neutral-400 mb-3">Table actions with tooltips:</p>
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg overflow-hidden">
<table class="w-full">
<thead class="bg-neutral-50 dark:bg-neutral-800">
<tr>
<th class="px-4 py-2 text-left text-sm font-medium text-neutral-700 dark:text-neutral-300">File</th>
<th class="px-4 py-2 text-left text-sm font-medium text-neutral-700 dark:text-neutral-300">Size</th>
<th class="px-4 py-2 text-right text-sm font-medium text-neutral-700 dark:text-neutral-300">Actions</th>
</tr>
</thead>
<tbody>
<tr class="border-t border-neutral-200 dark:border-neutral-700">
<td class="px-4 py-2 text-sm">document.pdf</td>
<td class="px-4 py-2 text-sm text-neutral-500">2.4 MB</td>
<td class="px-4 py-2 text-right">
<div class="inline-flex gap-1">
<button class="p-1 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-content="View file"
data-tooltip-placement-value="top"
data-tooltip-delay-value="300">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="24" height="24" viewBox="0 0 24 24"><g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path><circle cx="12" cy="12" r="3"></circle></g></svg>
</button>
<button class="p-1 rounded hover:bg-neutral-100 dark:hover:bg-neutral-700"
data-controller="tooltip"
data-tooltip-content="Download file"
data-tooltip-placement-value="top"
data-tooltip-delay-value="300">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="24" height="24" viewBox="0 0 24 24"><g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><path d="M7 10L12 15 17 10"></path><path d="M12 15L12 3"></path></g></svg>
</button>
<button class="p-1 rounded hover:bg-red-100 dark:hover:bg-red-900/20 text-red-600"
data-controller="tooltip"
data-tooltip-content="Delete file"
data-tooltip-placement-value="top"
data-tooltip-delay-value="300">
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" width="24" height="24" viewBox="0 0 24 24"><g stroke-linejoin="round" stroke-linecap="round" stroke-width="2" fill="none" stroke="currentColor"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></g></svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
Configuration
The tooltip component uses Floating UI for intelligent positioning and provides extensive customization options through a Stimulus controller.
Controller Setup
Basic tooltip structure with required data attributes:
<button data-controller="tooltip"
data-tooltip-content="Hello world"
data-tooltip-placement-value="top">
Hover me
</button>
Configuration Values
Prop | Description | Type | Default |
---|---|---|---|
placement
|
Position of the tooltip relative to trigger element. Supports all 12 placements |
String
|
"top"
|
offset
|
Distance between tooltip and trigger element in pixels |
Number
|
8
|
maxWidth
|
Maximum width of the tooltip in pixels |
Number
|
200
|
delay
|
Delay before showing the tooltip in milliseconds |
Number
|
0
|
size
|
Text size: small (text-xs), regular (text-sm), or large (text-base) |
String
|
"regular"
|
animation
|
Animation type: fade, origin, fade origin, or none |
String
|
"fade"
|
trigger
|
Event(s) that trigger the tooltip (space-separated) |
String
|
"mouseenter focus"
|
Data Attributes
Attribute | Description | Required |
---|---|---|
data-tooltip-content
|
The text content to display in the tooltip | Required |
data-tooltip-arrow
|
Whether to show the pointing arrow. Set to "false" to hide | Optional |
Animation Types
Type | Description |
---|---|
fade
|
Simple opacity transition (default) |
origin
|
Scales from 95% to 100% size, appearing to grow from the placement direction |
fade origin
|
Combines both fade and scale animations |
none
|
Instant appearance without any transition effects |
Accessibility Features
- Keyboard Support: Tooltips can be triggered with keyboard focus
- Screen Reader Friendly: Tooltip content is accessible to assistive technologies
- Auto-hide: Tooltips automatically hide when trigger element scrolls out of view
- Non-interactive: Tooltips don't block mouse events, maintaining UI accessibility
Best Practices
- Keep it concise: Tooltips should contain brief, helpful information
- Avoid essential information: Don't put critical information only in tooltips
- Use appropriate delays: Add delays for dense UIs to prevent tooltip spam