Tooltip

Component

Premium Tailwind tooltip components providing essential, non-disruptive context with elegant Tailwind CSS animations.

Install via CLI

Run this command to automatically add the component and its dependencies to your project.

npx @abhaysinghr516/business-wish add tooltip
New to the CLI? Run npx @abhaysinghr516/business-wish init first to initialize your project.

When space is at a premium, a Tailwind tooltip provides essential context exactly when the user needs it. These Tailwind CSS tooltip components activate on hover or focus, delivering helpful hints via fluid, non-disruptive scale animations and adaptive positioning.

If you need to display rich HTML content or interactive elements, upgrade to a Popover, or simply wrap a standard Button with a tooltip for quick contextual labels.

Basic Tooltip

import React, { useState } from "react";
interface TooltipProps {
  text: string;
  position?: "top" | "right" | "bottom" | "left";
  children: React.ReactNode;
}

const Tooltip: React.FC<TooltipProps> = ({ text, children }) => {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div className="relative inline-flex">
      <div
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </div>
      <div
        className={`
          absolute z-50 px-3 py-1.5 text-xs font-medium text-neutral-900 dark:text-white bg-white dark:bg-neutral-900
          rounded-md shadow-[0_8px_30px_rgb(0,0,0,0.12)] border border-neutral-200/60 dark:border-white/10
          transition-all duration-300 ease-out
          ${isVisible ? "opacity-100 translate-y-0 scale-100" : "opacity-0 translate-y-1 scale-95 pointer-events-none"}
          bottom-full left-1/2 transform -translate-x-1/2 -translate-y-2.5
        `}
        style={{ whiteSpace: "nowrap" }}
      >
        {text}
      </div>
    </div>
  );
};

const BasicTooltip = () => {
  return (
    <div className="flex items-center justify-center py-24 bg-neutral-50 dark:bg-[#0A0A0A]">
      <Tooltip text="Classic minimalistic tooltip">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all duration-300 shadow-sm">
          Hover me
        </button>
      </Tooltip>
    </div>
  );
};

export default BasicTooltip;

Arrow Tooltip

import React, { useEffect, useRef, useState } from "react";
interface TooltipProps {
  text: string;
  position?: "top" | "right" | "bottom" | "left";
  children: React.ReactNode;
}

const TooltipArrow: React.FC<TooltipProps> = ({
  text,
  position = "top",
  children,
}) => {
  const [isVisible, setIsVisible] = useState(false);
  const tooltipRef = useRef<HTMLDivElement>(null);
  const targetRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const updatePosition = () => {
      if (isVisible && tooltipRef.current && targetRef.current) {
        const targetRect = targetRef.current.getBoundingClientRect();
        const tooltipRect = tooltipRef.current.getBoundingClientRect();
        let top = 0;
        let left = 0;

        switch (position) {
          case "top":
            top = -tooltipRect.height - 12;
            left = (targetRect.width - tooltipRect.width) / 2;
            break;
          case "right":
            top = (targetRect.height - tooltipRect.height) / 2;
            left = targetRect.width + 12;
            break;
          case "bottom":
            top = targetRect.height + 12;
            left = (targetRect.width - tooltipRect.width) / 2;
            break;
          case "left":
            top = (targetRect.height - tooltipRect.height) / 2;
            left = -tooltipRect.width - 12;
            break;
        }

        tooltipRef.current.style.top = `${top}px`;
        tooltipRef.current.style.left = `${left}px`;
      }
    };

    updatePosition();
    window.addEventListener("resize", updatePosition);
    return () => window.removeEventListener("resize", updatePosition);
  }, [isVisible, position]);

  return (
    <div className="relative inline-flex">
      <div
        ref={targetRef}
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </div>
      <div
        ref={tooltipRef}
        className={`
          absolute z-50 px-3 py-1.5 text-xs font-medium text-neutral-900 dark:text-white bg-white dark:bg-neutral-900
          rounded-md shadow-[0_8px_30px_rgb(0,0,0,0.12)] border border-neutral-200/60 dark:border-white/10
          transition-all duration-300 ease-out
          ${isVisible ? "opacity-100 scale-100" : "opacity-0 scale-95 pointer-events-none"}
          tooltip-${position}
        `}
        style={{ whiteSpace: "nowrap" }}
      >
        {text}
        <div
          className={`
            absolute w-2.5 h-2.5 bg-white dark:bg-neutral-900 border-neutral-200/60 dark:border-white/10
            transform rotate-45 pointer-events-none
            ${
              position === "top"
                ? "bottom-0 left-1/2 -translate-x-1/2 translate-y-[5px] border-b border-r"
                : position === "right"
                ? "left-0 top-1/2 -translate-y-1/2 -translate-x-[5px] border-b border-l"
                : position === "bottom"
                ? "top-0 left-1/2 -translate-x-1/2 -translate-y-[5px] border-t border-l"
                : "right-0 top-1/2 -translate-y-1/2 translate-x-[5px] border-t border-r"
            }
          `}
        />
      </div>
    </div>
  );
};

const ArrowTooltip = () => {
  return (
    <div className="flex flex-col sm:flex-row items-center justify-center gap-6 py-32 bg-neutral-50 dark:bg-[#0A0A0A]">
      <TooltipArrow text="Left tooltip" position="left">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm w-[100px]">
          Left
        </button>
      </TooltipArrow>
      <TooltipArrow text="Top tooltip" position="top">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm w-[100px]">
          Top
        </button>
      </TooltipArrow>
      <TooltipArrow text="Bottom tooltip" position="bottom">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm w-[100px]">
          Bottom
        </button>
      </TooltipArrow>
      <TooltipArrow text="Right tooltip" position="right">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm w-[100px]">
          Right
        </button>
      </TooltipArrow>
    </div>
  );
};

export default ArrowTooltip;

Animated Tooltip

import React, { useState } from "react";
interface TooltipProps {
  text: string;
  position?: "top" | "right" | "bottom" | "left";
  children: React.ReactNode;
}

const TooltipAnimated: React.FC<TooltipProps> = ({ text, children }) => {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div className="relative inline-flex">
      <div
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </div>
      <div
        className={`
          absolute z-50 px-3 py-1.5 text-xs font-medium text-neutral-900 dark:text-white bg-white dark:bg-neutral-900
          rounded-md shadow-[0_8px_30px_rgb(0,0,0,0.12)] border border-neutral-200/60 dark:border-white/10
          transition-all duration-400 ease-[cubic-bezier(0.16,1,0.3,1)]
          transform origin-bottom
          ${
            isVisible
              ? "opacity-100 visible translate-y-0 scale-100"
              : "opacity-0 invisible translate-y-3 scale-75 pointer-events-none"
          }
          bottom-full left-1/2 -translate-x-1/2 -translate-y-3
        `}
        style={{ whiteSpace: "nowrap" }}
      >
        {text}
      </div>
    </div>
  );
};

const AnimatedTooltip = () => {
  return (
    <div className="flex items-center justify-center py-24 bg-neutral-50 dark:bg-[#0A0A0A]">
      <TooltipAnimated text="Springy entry animation">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-full hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all duration-300 shadow-sm">
          Hover for spring
        </button>
      </TooltipAnimated>
    </div>
  );
};

export default AnimatedTooltip;

Adaptive Tooltip

Adaptive tooltip that changes direction based on available space.

import React, { useEffect, useRef, useState } from "react";
interface TooltipProps {
  text: string;
  position?: "top" | "right" | "bottom" | "left";
  children: React.ReactNode;
}

type Direction = "auto" | "top" | "right" | "bottom" | "left";

const TooltipAdaptive: React.FC<TooltipProps & { direction?: Direction }> = ({
  text,
  children,
  direction = "auto",
}) => {
  const [isVisible, setIsVisible] = useState(false);
  const [currentDirection, setCurrentDirection] =
    useState<Direction>(direction);
  const tooltipRef = useRef<HTMLDivElement>(null);
  const targetRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const updatePosition = () => {
      if (
        isVisible &&
        tooltipRef.current &&
        targetRef.current &&
        direction === "auto"
      ) {
        const targetRect = targetRef.current.getBoundingClientRect();
        const tooltipRect = tooltipRef.current.getBoundingClientRect();
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;

        let newDirection: Direction = "top";

        if (targetRect.top > tooltipRect.height + 12) {
          newDirection = "top";
        } else if (viewportWidth - targetRect.right > tooltipRect.width + 12) {
          newDirection = "right";
        } else if (viewportHeight - targetRect.bottom > tooltipRect.height + 12) {
          newDirection = "bottom";
        } else if (targetRect.left > tooltipRect.width + 12) {
          newDirection = "left";
        }

        setCurrentDirection(newDirection);
      } else {
        setCurrentDirection(direction);
      }
    };

    updatePosition();
    window.addEventListener("resize", updatePosition);
    return () => window.removeEventListener("resize", updatePosition);
  }, [isVisible, direction]);

  const getTooltipStyles = () => {
    switch (currentDirection) {
      case "top":
        return "bottom-full left-1/2 transform -translate-x-1/2 -translate-y-3 origin-bottom";
      case "right":
        return "top-1/2 left-full transform translate-x-3 -translate-y-1/2 origin-left";
      case "bottom":
        return "top-full left-1/2 transform -translate-x-1/2 translate-y-3 origin-top";
      case "left":
        return "top-1/2 right-full transform -translate-x-3 -translate-y-1/2 origin-right";
      default:
        return "bottom-full left-1/2 transform -translate-x-1/2 -translate-y-3 origin-bottom";
    }
  };

  return (
    <div className="relative inline-flex">
      <div
        ref={targetRef}
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </div>
      <div
        ref={tooltipRef}
        className={`
          absolute z-50 px-3 py-1.5 text-xs font-medium text-neutral-900 dark:text-white bg-white dark:bg-neutral-900
          rounded-md shadow-[0_8px_30px_rgb(0,0,0,0.12)] border border-neutral-200/60 dark:border-white/10
          transition-all duration-300 ease-out
          ${isVisible ? "opacity-100 scale-100" : "opacity-0 scale-95 pointer-events-none"}
          ${getTooltipStyles()}
        `}
        style={{ whiteSpace: "nowrap" }}
      >
        {text}
        <div
          className={`
            absolute w-2.5 h-2.5 bg-white dark:bg-neutral-900 border-neutral-200/60 dark:border-white/10
            transform rotate-45 pointer-events-none
            ${
              currentDirection === "top"
                ? "bottom-0 left-1/2 -translate-x-1/2 translate-y-[5px] border-b border-r"
                : currentDirection === "right"
                ? "left-0 top-1/2 -translate-y-1/2 -translate-x-[5px] border-b border-l"
                : currentDirection === "bottom"
                ? "top-0 left-1/2 -translate-x-1/2 -translate-y-[5px] border-t border-l"
                : "right-0 top-1/2 -translate-y-1/2 translate-x-[5px] border-t border-r"
            }
          `}
        />
      </div>
    </div>
  );
};

const TooltipDemo = () => {
  const [globalDirection, setGlobalDirection] = useState<Direction>("auto");

  const getTooltipDirection = (defaultDirection: Direction): Direction => {
    return globalDirection === "auto" ? defaultDirection : globalDirection;
  };

  return (
    <div className="flex flex-col items-center justify-center py-24 space-y-10 px-4 bg-neutral-50 dark:bg-[#0A0A0A]">
      <div className="flex p-1 bg-neutral-200/50 dark:bg-neutral-800/50 rounded-lg drop-shadow-sm border border-neutral-200/50 dark:border-white/5">
        {["auto", "top", "right", "bottom", "left"].map((dir) => (
          <button
            key={dir}
            onClick={() => setGlobalDirection(dir as Direction)}
            className={`
              px-4 py-1.5 rounded-md transition-all duration-300 text-[13px] font-medium
              ${
                globalDirection === dir
                  ? "bg-white dark:bg-neutral-700 text-neutral-900 dark:text-white shadow-sm"
                  : "text-neutral-500 hover:text-neutral-700 dark:hover:text-neutral-300"
              }
            `}
          >
            {dir.charAt(0).toUpperCase() + dir.slice(1)}
          </button>
        ))}
      </div>
      <div className="grid grid-cols-3 gap-6 w-full max-w-sm">
        <div></div>
        <TooltipAdaptive
          text="Top adaptive"
          direction={getTooltipDirection("top")}
        >
          <button className="w-full px-4 py-2 flex text-[14px] items-center justify-center font-medium text-neutral-500 dark:text-neutral-400 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-2xl hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm">
            Top
          </button>
        </TooltipAdaptive>
        <div></div>
        <TooltipAdaptive
          text="Left adaptive"
          direction={getTooltipDirection("left")}
        >
          <button className="w-full px-4 py-2 flex text-[14px] items-center justify-center font-medium text-neutral-500 dark:text-neutral-400 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-2xl hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm">
            Left
          </button>
        </TooltipAdaptive>
        <div className="flex items-center justify-center">
          <div className="text-xs text-neutral-400 dark:text-neutral-500 uppercase tracking-widest font-semibold">
            {globalDirection}
          </div>
        </div>
        <TooltipAdaptive
          text="Right adaptive"
          direction={getTooltipDirection("right")}
        >
          <button className="w-full px-4 py-2 flex text-[14px] items-center justify-center font-medium text-neutral-500 dark:text-neutral-400 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-2xl hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm">
            Right
          </button>
        </TooltipAdaptive>
        <div></div>
        <TooltipAdaptive
          text="Bottom adaptive"
          direction={getTooltipDirection("bottom")}
        >
          <button className="w-full px-4 py-2 flex text-[14px] items-center justify-center font-medium text-neutral-500 dark:text-neutral-400 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-2xl hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all shadow-sm">
            Bottom
          </button>
        </TooltipAdaptive>
        <div></div>
      </div>
    </div>
  );
};

export default TooltipDemo;

Offset Tooltip

A distinct, subtle offset design showcasing an independent "widget" style without the typical arrow.

import React, { useState } from "react";
interface TooltipProps {
  text: string;
  position?: "top" | "right" | "bottom" | "left";
  children: React.ReactNode;
}

const TooltipOffset: React.FC<TooltipProps> = ({ text, children }) => {
  const [isVisible, setIsVisible] = useState(false);

  return (
    <div className="relative inline-flex drop-shadow-sm">
      <div
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </div>
      <div
        className={`
          absolute z-50 px-4 py-2 text-xs font-medium text-white
          bg-neutral-900 dark:bg-neutral-800
          rounded-xl shadow-xl ring-1 ring-white/20
          transition-all duration-300 ease-out
          ${isVisible ? "opacity-100 translate-y-0 scale-100" : "opacity-0 translate-y-2 scale-95 pointer-events-none"}
          bottom-full left-1/2 transform -translate-x-1/2 -translate-y-4
        `}
        style={{ whiteSpace: "nowrap" }}
      >
        <div className="absolute inset-0 rounded-xl bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
        <span className="relative z-10 font-semibold tracking-wide">
          {text}
        </span>
      </div>
    </div>
  );
};

export function OffsetTooltip() {
  return (
    <div className="flex items-center justify-center py-24 bg-neutral-50 dark:bg-[#0A0A0A]">
      <TooltipOffset text="⌘ + K">
        <button className="px-5 py-2.5 text-[14px] font-medium text-neutral-700 dark:text-neutral-200 bg-white dark:bg-neutral-900 border border-neutral-200/60 dark:border-white/10 rounded-xl hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-all duration-300 shadow-sm">
          Search
        </button>
      </TooltipOffset>
    </div>
  );
}