React tooltip component that just works
I wanted to add a tooltip to my product. After exploring several npm React tooltip libraries, I realized that not only were these solutions complex to implement, but styling them also became a cumbersome process. That's when I decided to create my own React tooltip component in Typescript and Tailwind, focusing on making it simple to use and customizable.
Features:
- Built with Typescript and Tailwind CSS. No other library is used.
- Dead-simple usage: Just wrap your component with the
Tooltip
(noref
needed). - Boundary-aware: Repositions itself to stay within the viewport when near a corner.
- Supports rich tooltip content: You can use React components.
- Fully functional on both desktop and mobile platforms.
- Customizable display delay (default set to 300ms).
- Includes an arrow indicator.
- Beautiful appearance by default.
How it Looks
I'm currently using this tooltip on my product's pricing page. To see it in action, hover over the question mark icon.
How to Use
Firstly, wrap your component with the Tooltip
. Hovering the mouse over it will display the tooltip.
Next, pass a string or a React Component as the content
prop to define the tooltip content when it becomes visible.
import { Tooltip } from "./tooltip";
<Tooltip content={<TooltipContent />}>
<button>Hover over me</button>
</Tooltip>
function TooltipContent() {
return (
<div>
This is a <span className="italic">boundary-aware</span>{" "}
<span className="font-bold">React Tooltip</span>
</div>
);
}
Live Demo
Check out the live demonstration at this CodeSandbox link.
Source Code
Create a tooltip.tsx
file and add this code:
// Author: https://gourav.io/blog/react-tooltip-component //
import { SVGProps, forwardRef, useEffect, useRef, useState, type ReactNode } from 'react';
/**
* content: use `<br/>` to break lines so that tooltip is not too wide
* @returns
*/
export const Tooltip = ({ content, children }: { content: ReactNode; children: ReactNode }) => {
const [hover, setHover] = useState(false);
const hoverTimeout = useRef<NodeJS.Timeout | null>(null);
const tooltipContentRef = useRef<HTMLDivElement>(null);
const triangleRef = useRef<SVGSVGElement>(null);
const triangleInvertedRef = useRef<SVGSVGElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
const delay = 300;
const handleMouseEnter = () => {
hoverTimeout.current = setTimeout(() => {
setHover(true);
}, delay);
};
const handleMouseLeave = () => {
if (hoverTimeout.current) {
clearTimeout(hoverTimeout.current);
hoverTimeout.current = null;
}
setHover(false);
};
const updateTooltipPosition = () => {
if (tooltipContentRef.current && tooltipRef.current && triangleRef.current && triangleInvertedRef.current) {
const rect = tooltipContentRef.current.getBoundingClientRect();
let { top, left, right } = rect;
const padding = 40;
// overflowing from left side
if (left < 0 + padding) {
const newLeft = Math.abs(left) + padding;
tooltipContentRef.current.style.left = `${newLeft}px`;
}
// overflowing from right side
else if (right + padding > window.innerWidth) {
const newRight = right + padding - window.innerWidth;
tooltipContentRef.current.style.right = `${newRight}px`;
}
// overflowing from top side
if (top < 0) {
// unset top and set bottom
tooltipRef.current.style.top = 'unset';
tooltipRef.current.style.bottom = '0';
tooltipRef.current.style.transform = 'translateY(calc(100% + 10px))';
triangleInvertedRef.current.style.display = 'none';
triangleRef.current.style.display = 'block';
}
}
};
// Update position on window resize
useEffect(() => {
const handleResize = () => {
if (hover) {
updateTooltipPosition();
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, [hover]);
return (
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className="relative inline-flex flex-col items-center ">
{hover && (
<div
ref={tooltipRef}
className="absolute left-0 top-0 mx-auto flex w-full items-center justify-center gap-0 [transform:translateY(calc(-100%-10px))] [z-index:999999]">
<div className="mx-auto flex w-0 flex-col items-center justify-center text-slate-800">
<TriangleFilled
ref={triangleRef}
style={{ marginBottom: '-7px', display: 'none' }}
/>
<div
ref={tooltipContentRef}
className="relative whitespace-nowrap rounded-md bg-slate-800 p-2.5 text-[14px] leading-relaxed tracking-wide text-white shadow-sm [font-weight:400]">
{content}
</div>
<TriangleInvertedFilled
ref={triangleInvertedRef}
style={{ marginTop: '-7px' }}
/>
</div>
</div>
)}
{children}
</div>
);
};
const TriangleInvertedFilled = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>((props, ref) => {
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}>
<g
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2">
<path d="M0 0h24v24H0z"></path>
<path
fill="currentColor"
d="M20.118 3H3.893A2.914 2.914 0 0 0 1.39 7.371L9.506 20.92a2.917 2.917 0 0 0 4.987.005l8.11-13.539A2.914 2.914 0 0 0 20.117 3z"></path>
</g>
</svg>
);
});
TriangleInvertedFilled.displayName = 'TriangleInvertedFilled';
const TriangleFilled = forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>((props, ref) => {
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}>
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2">
<path d="M0 0h24v24H0z"></path>
<path
fill="currentColor"
d="M12 1.67a2.914 2.914 0 0 0-2.492 1.403L1.398 16.61a2.914 2.914 0 0 0 2.484 4.385h16.225a2.914 2.914 0 0 0 2.503-4.371L14.494 3.078A2.917 2.917 0 0 0 12 1.67"></path>
</g>
</svg>
);
});
TriangleFilled.displayName = 'TriangleFilled';