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 this.closingTooltips = new Set(); // Track tooltips currently in closing animation } // Called when a tooltip becomes visible onTooltipShow(tooltipController) { this.visibleTooltips.add(tooltipController); this.closingTooltips.delete(tooltipController); // Remove from closing if it was there this.visibleCount = this.visibleTooltips.size; if (this.visibleCount > 0 && !this.isFastMode) { this.isFastMode = true; } this.clearResetTimeout(); } // Called when a tooltip starts hiding onTooltipStartHide(tooltipController) { this.visibleTooltips.delete(tooltipController); this.visibleCount = this.visibleTooltips.size; } // Called when a tooltip starts its closing animation onTooltipClosing(tooltipController) { this.closingTooltips.add(tooltipController); } // Called when a tooltip has fully closed onTooltipClosed(tooltipController) { this.closingTooltips.delete(tooltipController); // If no tooltips are visible or closing, start countdown to exit fast mode if (this.visibleCount === 0 && this.closingTooltips.size === 0) { this.startResetTimeout(); } } // Hide all currently visible tooltips and interrupt closing animations hideAllTooltipsInstantly(exceptController) { // Instantly hide all visible tooltips const visibleToHide = [...this.visibleTooltips].filter((controller) => controller !== exceptController); visibleToHide.forEach((controller) => { controller._hideTooltip(true); // true = instant hide }); // Instantly finish all closing animations const closingToHide = [...this.closingTooltips].filter((controller) => controller !== exceptController); closingToHide.forEach((controller) => { controller._finishClosingAnimation(); }); } // 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: "auto" }, // space-separated: mouseenter, focus, click, or "auto" (auto-detects based on device) // 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); this.clickToggleTooltipBound = this._handleClickToggle.bind(this); this.clickOutsideBound = this._handleClickOutside.bind(this); // Auto-detect trigger based on device capability let triggerValue = this.triggerValue; if (triggerValue === "auto") { // Use click on touch devices, mouseenter+focus on others const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0; triggerValue = isTouchDevice ? "click" : "mouseenter focus"; } const triggers = triggerValue.split(" "); this.hasMouseEnterTrigger = triggers.includes("mouseenter"); this.hasClickTrigger = triggers.includes("click"); 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); } if (event_type === "click") { this.element.addEventListener("click", this.clickToggleTooltipBound); } }); // Add click event to close tooltip but allow event to bubble up (only for hover triggers) if (this.hasMouseEnterTrigger && !this.hasClickTrigger) { this.element.addEventListener("click", this.clickHideTooltipBound); } this.cleanupAutoUpdate = null; this.intersectionObserver = null; } disconnect() { clearTimeout(this.showTimeoutId); clearTimeout(this.hideTimeoutId); // Remove from global state if visible or closing if (this.isVisible) { tooltipGlobalState.onTooltipStartHide(this); this.isVisible = false; } tooltipGlobalState.onTooltipClosed(this); // Auto-detect trigger (same logic as connect) let triggerValue = this.triggerValue; if (triggerValue === "auto") { const isTouchDevice = "ontouchstart" in window || navigator.maxTouchPoints > 0; triggerValue = isTouchDevice ? "click" : "mouseenter focus"; } 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); } if (event_type === "click") { this.element.removeEventListener("click", this.clickToggleTooltipBound); } }); // Remove click event listener only if it was added for hover-enabled tooltips if (this.hasMouseEnterTrigger && !this.hasClickTrigger) { this.element.removeEventListener("click", this.clickHideTooltipBound); } this._cleanupObservers(); if (this.tooltipElement && this.tooltipElement.parentElement) { this.tooltipElement.remove(); } } 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 // Always hide all other visible tooltips and interrupt closing animations IMMEDIATELY // This must happen synchronously before scheduling the show to prevent multiple tooltips // from appearing simultaneously when hovering quickly tooltipGlobalState.hideAllTooltipsInstantly(this); // 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 () => { // 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 const applyVisibleState = () => { this.tooltipElement.classList.remove("opacity-0"); this.tooltipElement.classList.add("opacity-100"); if (this._hasAnimationType("origin")) { this.tooltipElement.classList.remove("scale-95"); this.tooltipElement.classList.add("scale-100"); } }; if (isFastMode) { // Fast mode: apply changes instantly without transitions this.tooltipElement.setAttribute("data-instant", ""); this._withoutTransition(applyVisibleState); // Remove data-instant after the instant show is complete // This prevents it from interfering with hover state changes requestAnimationFrame(() => { if (this.tooltipElement) { this.tooltipElement.removeAttribute("data-instant"); } }); } else { // Normal mode: use requestAnimationFrame for smooth animations this.tooltipElement.removeAttribute("data-instant"); requestAnimationFrame(applyVisibleState); } // 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); } // 7. Add click-outside listener for click triggers if (this.hasClickTrigger) { // Use setTimeout to avoid immediately triggering the click-outside handler setTimeout(() => { document.addEventListener("click", this.clickOutsideBound); }, 0); } }, 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) } _handleClickToggle(event) { // Toggle tooltip visibility on click if (this.isVisible) { this._hideTooltip(); } else { this._showTooltip(); } // Prevent the click from bubbling only if the element is interactive (button, a, etc.) // Otherwise allow it to bubble for non-interactive elements const isInteractive = this.element.matches("button, a, [role='button'], input, select, textarea"); if (!isInteractive) { event.stopPropagation(); } } _handleClickOutside(event) { // Hide tooltip when clicking outside of trigger element if (!this.element.contains(event.target)) { this._hideTooltip(); } } // Helper: Apply instant changes without transitions _withoutTransition(callback) { if (!this.tooltipElement) return; // Ensure data-instant is set (caller should set it, but we ensure it's there) this.tooltipElement.setAttribute("data-instant", ""); this.tooltipElement.offsetHeight; // Force reflow callback(); // Don't remove data-instant here - let the show/hide logic manage it // This keeps the attribute on for the entire duration of instant mode } // Helper: Apply hidden state (opacity, scale, visibility) _applyHiddenState() { if (!this.tooltipElement) return; 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"; } // Helper: Cleanup observers and auto-update _cleanupObservers() { if (this.cleanupAutoUpdate) { this.cleanupAutoUpdate(); this.cleanupAutoUpdate = null; } if (this.intersectionObserver) { this.intersectionObserver.disconnect(); this.intersectionObserver = null; } // Remove click-outside listener if (this.hasClickTrigger) { document.removeEventListener("click", this.clickOutsideBound); } } _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 starting to hide if (this.isVisible) { this.isVisible = false; tooltipGlobalState.onTooltipStartHide(this); } this._cleanupObservers(); if (isInstantHide) { // Instant hide: apply hidden state without transitions this.tooltipElement.setAttribute("data-instant", ""); this._withoutTransition(() => { this._applyHiddenState(); }); tooltipGlobalState.onTooltipClosed(this); return; } // Remove data-instant for normal animated hide this.tooltipElement.removeAttribute("data-instant"); // Normal hide with animations tooltipGlobalState.onTooltipClosing(this); const needsAnimation = this._hasAnimationType("fade") || this._hasAnimationType("origin"); if (needsAnimation || this.animationValue === "none") { // Apply opacity/scale changes for animation 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"); } } // Calculate animation duration const animationDelay = needsAnimation ? 100 : 0; // Both fade and origin use 100ms this.hideTimeoutId = setTimeout(() => { if (this.tooltipElement) { this.tooltipElement.style.visibility = "hidden"; } tooltipGlobalState.onTooltipClosed(this); }, animationDelay); } // Instantly finish a closing animation (called when another tooltip is being shown) _finishClosingAnimation() { clearTimeout(this.hideTimeoutId); if (!this.tooltipElement) return; this.tooltipElement.setAttribute("data-instant", ""); this._withoutTransition(() => { this._applyHiddenState(); }); tooltipGlobalState.onTooltipClosed(this); } } 

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"
Terminal
npm install @floating-ui/dom
Terminal
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.

📱 Mobile Support

By default, tooltips use auto mode which automatically detects the device type. On mobile/touch devices, tooltips use click triggers. On desktop, they use hover and focus triggers for a better experience.

Focus triggers are useful for form elements:

<%# Different tooltip trigger events %> <div class="mb-4 rounded-lg bg-blue-50 p-4 text-sm text-blue-800 dark:bg-blue-900/30 dark:text-blue-200"> <p class="font-medium mb-1">📱 Mobile Support</p> <p>By default, tooltips use <strong>auto</strong> mode which automatically detects the device type. On mobile/touch devices, tooltips use click triggers. On desktop, they use hover and focus triggers for a better experience.</p> </div> <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" data-tooltip-placement-value="top" data-tooltip-trigger-value="mouseenter focus"> Hover & Focus </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="Click to toggle tooltip - perfect for mobile!" data-tooltip-placement-value="top" data-tooltip-trigger-value="click"> Click to Toggle </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="Auto mode uses click on mobile, hover on desktop (default)" data-tooltip-placement-value="top" data-tooltip-trigger-value="auto"> Auto (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:

Online
Maintenance
Offline

Truncated text with full content in tooltip:

This is a very long filename that would normally be truncated in the UI but can be viewed in full using this tooltip.
/Users/johndoe/Documents/Projects/2024/ClientWork/AcmeCorporation/Proposals/Q4/Final/approved_proposal_v3_final_final.docx

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: auto (detects device), mouseenter, focus, click, or space-separated combination String "auto"

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

Table of contents