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";

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
    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;

    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-250"); // 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-100", "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.triggerValue.split(" ").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);
      }
    });

    this.cleanupAutoUpdate = null;
    this.intersectionObserver = null;
  }

  disconnect() {
    clearTimeout(this.showTimeoutId);
    clearTimeout(this.hideTimeoutId); // Clear hideTimeoutId on disconnect

    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);
      }
    });

    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

    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
      requestAnimationFrame(() => {
        // Use rAF for smoother start to animations
        let applyOpacity100 = false;
        let applyScale100 = false;

        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");
        }
      });

      // 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);
    }, this.delayValue);
  }

  _hideTooltip() {
    clearTimeout(this.showTimeoutId); // Cancel any pending show operation
    clearTimeout(this.hideTimeoutId); // Cancel any pending hide finalization

    if (!this.tooltipElement) return;

    // Start the hide animation by applying target opacity/transform
    let targetOpacity0 = false;
    let targetScale95 = false;

    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 (this._hasAnimationType("fade")) {
      applicableHideDelay = Math.max(applicableHideDelay, 250); // Fade duration
    }
    if (this._hasAnimationType("origin")) {
      applicableHideDelay = Math.max(applicableHideDelay, 250); // Origin duration
    }
    // If animation is "none", applicableHideDelay remains 0, so timeout is effectively immediate.

    this.hideTimeoutId = setTimeout(() => {
      if (this.tooltipElement) {
        // Only hide visibility if there was an animation to wait for.
        // For "none", visibility is handled when animation classes are applied.
        if (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/[email protected]/+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.

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 gap-1 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-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-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-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-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-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:

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 (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

Table of contents