Tooltip
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 tooltipnpx @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>
);
}