Tooltip
Tooltip is a small pop-up box that appears when a user places the mouse pointer over an element.
Basic Tooltip
import { useState } from 'react'
interface TooltipProps {
text: string
children: React.ReactNode
}
const Tooltip: React.FC<TooltipProps> = ({ text, children }) => {
const [isVisible, setIsVisible] = useState(false)
return (
<div className="relative inline-block">
<div
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</div>
{isVisible && (
<div className="absolute z-10 px-3 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-gray-800 rounded-lg shadow-sm bottom-full left-1/2 transform -translate-x-1/2 -translate-y-2">
{text}
</div>
)}
</div>
)
}
export default function Component() {
return (
<div className="flex items-center justify-center h-32">
<Tooltip text="This is a simple tooltip">
<button className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700">
Hover me
</button>
</Tooltip>
</div>
)
}
Arrow Tooltip
import React, { useState, useRef, useEffect } from 'react'
interface TooltipProps {
text: string
position?: 'top' | 'right' | 'bottom' | 'left'
children: React.ReactNode
}
const Tooltip: 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 - 10
left = (targetRect.width - tooltipRect.width) / 2
break
case 'right':
top = (targetRect.height - tooltipRect.height) / 2
left = targetRect.width + 10
break
case 'bottom':
top = targetRect.height + 10
left = (targetRect.width - tooltipRect.width) / 2
break
case 'left':
top = (targetRect.height - tooltipRect.height) / 2
left = -tooltipRect.width - 10
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-block">
<div
ref={targetRef}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</div>
{isVisible && (
<div
ref={tooltipRef}
className={`absolute z-10 px-3 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-gray-800 rounded-lg shadow-sm tooltip-${position}`}
style={{ whiteSpace: 'nowrap' }}
>
{text}
<div
className={`absolute w-2 h-2 bg-gray-900 dark:bg-gray-800 transform rotate-45 ${
position === 'top' ? 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2' :
position === 'right' ? 'left-0 top-1/2 -translate-y-1/2 -translate-x-1/2' :
position === 'bottom' ? 'top-0 left-1/2 -translate-x-1/2 -translate-y-1/2' :
'right-0 top-1/2 -translate-y-1/2 translate-x-1/2'
}`}
></div>
</div>
)}
</div>
)
}
export default function Component() {
return (
<div className="flex items-center justify-center space-x-4 h-64">
<Tooltip text="Top tooltip" position="top">
<button className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700">
Top
</button>
</Tooltip>
<Tooltip text="Right tooltip" position="right">
<button className="px-4 py-2 text-white bg-green-500 rounded hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700">
Right
</button>
</Tooltip>
<Tooltip text="Bottom tooltip" position="bottom">
<button className="px-4 py-2 text-white bg-red-500 rounded hover:bg-red-600 dark:bg-red-600 dark:hover:bg-red-700">
Bottom
</button>
</Tooltip>
<Tooltip text="Left tooltip" position="left">
<button className="px-4 py-2 text-white bg-purple-500 rounded hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700">
Left
</button>
</Tooltip>
</div>
)
}
Animated Tooltip
import { useState } from 'react'
interface TooltipProps {
text: string
children: React.ReactNode
}
const Tooltip: React.FC<TooltipProps> = ({ text, children }) => {
const [isVisible, setIsVisible] = useState(false)
return (
<div className="relative inline-block">
<div
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</div>
<div
className={`
absolute z-10 px-3 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-gray-800 rounded-lg shadow-sm
transition-all duration-300 ease-in-out
${isVisible ? 'opacity-100 visible' : 'opacity-0 invisible'}
bottom-full left-1/2 transform -translate-x-1/2 -translate-y-2
`}
>
{text}
</div>
</div>
)
}
export default function Component() {
return (
<div className="flex items-center justify-center h-32">
<Tooltip text="This tooltip fades in and out">
<button className="px-4 py-2 text-white bg-indigo-500 rounded hover:bg-indigo-600 dark:bg-indigo-600 dark:hover:bg-indigo-700">
Hover for animated tooltip
</button>
</Tooltip>
</div>
)
}
Multi Direction Tooltip
Adaptive tooltip that changes direction based on available space.
"use client"
import { useState, useRef, useEffect } from 'react'
type Direction = 'top' | 'right' | 'bottom' | 'left'
interface TooltipProps {
text: string
children: React.ReactNode
}
const Tooltip: React.FC<TooltipProps> = ({ text, children }) => {
const [isVisible, setIsVisible] = useState(false)
const [direction, setDirection] = useState<Direction>('top')
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()
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
let newDirection: Direction = 'top'
if (targetRect.top > tooltipRect.height) {
newDirection = 'top'
} else if (viewportWidth - targetRect.right > tooltipRect.width) {
newDirection = 'right'
} else if (viewportHeight - targetRect.bottom > tooltipRect.height) {
newDirection = 'bottom'
} else if (targetRect.left > tooltipRect.width) {
newDirection = 'left'
}
setDirection(newDirection)
}
}
updatePosition()
window.addEventListener('resize', updatePosition)
return () => window.removeEventListener('resize', updatePosition)
}, [isVisible])
const getTooltipStyles = () => {
switch (direction) {
case 'top':
return 'bottom-full left-1/2 transform -translate-x-1/2 -translate-y-2'
case 'right':
return 'top-1/2 left-full transform translate-x-2 -translate-y-1/2'
case 'bottom':
return 'top-full left-1/2 transform -translate-x-1/2 translate-y-2'
case 'left':
return 'top-1/2 right-full transform -translate-x-2 -translate-y-1/2'
}
}
return (
<div className="relative inline-block">
<div
ref={targetRef}
onMouseEnter={() => setIsVisible(true)}
onMouseLeave={() => setIsVisible(false)}
>
{children}
</div>
{isVisible && (
<div
ref={tooltipRef}
className={`
absolute z-10 px-3 py-2 text-sm font-medium text-white bg-gray-900 dark:bg-gray-800 rounded-lg shadow-sm
${getTooltipStyles()}
`}
>
{text}
<div
className={`absolute w-3 h-3 bg-gray-900 dark:bg-gray-800 transform rotate-45 ${
direction === 'top' ? 'bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2' :
direction === 'right' ? 'left-0 top-1/2 -translate-y-1/2 -translate-x-1/2' :
direction === 'bottom' ? 'top-0 left-1/2 -translate-x-1/2 -translate-y-1/2' :
'right-0 top-1/2 -translate-y-1/2 translate-x-1/2'
}`}
></div>
</div>
)}
</div>
)
}
export default function Component() {
return (
<div className="flex items-center justify-center h-64">
<Tooltip text="This tooltip adapts to available space">
<button className="px-4 py-2 text-white bg-teal-500 rounded hover:bg-teal-600">
Hover for multi-direction tooltip
</button>
</Tooltip>
</div>
)
}