Tooltip
A flexible tooltip component for displaying contextual information on hover or click. Built with React and styled with Tailwind CSS with smart positioning and smooth animations.
Component Code
import React, { useState, useRef, useEffect } from "react";
const Tooltip = ({ children, content, position = "top", trigger = "hover", delay = 500, className = "", contentClassName = "", arrow = true, disabled = false, maxWidth = "max-w-xs",}) => { const [isVisible, setIsVisible] = useState(false); const [actualPosition, setActualPosition] = useState(position); const tooltipRef = useRef(null); const triggerRef = useRef(null); const timeoutRef = useRef(null);
useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []);
const showTooltip = () => { if (disabled) return;
if (delay > 0) { timeoutRef.current = setTimeout(() => { setIsVisible(true); updatePosition(); }, delay); } else { setIsVisible(true); updatePosition(); } };
const hideTooltip = () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } setIsVisible(false); };
const toggleTooltip = () => { if (isVisible) { hideTooltip(); } else { showTooltip(); } };
const updatePosition = () => { if (!tooltipRef.current || !triggerRef.current) return;
const triggerRect = triggerRef.current.getBoundingClientRect(); const tooltipRect = tooltipRef.current.getBoundingClientRect(); const viewport = { width: window.innerWidth, height: window.innerHeight, };
let newPosition = position;
// Check if tooltip would overflow and adjust position switch (position) { case "top": if (triggerRect.top - tooltipRect.height < 10) { newPosition = "bottom"; } break; case "bottom": if (triggerRect.bottom + tooltipRect.height > viewport.height - 10) { newPosition = "top"; } break; case "left": if (triggerRect.left - tooltipRect.width < 10) { newPosition = "right"; } break; case "right": if (triggerRect.right + tooltipRect.width > viewport.width - 10) { newPosition = "left"; } break; }
setActualPosition(newPosition); };
const getPositionStyles = () => { const positions = { top: "bottom-full left-1/2 transform -translate-x-1/2 mb-2", bottom: "top-full left-1/2 transform -translate-x-1/2 mt-2", left: "right-full top-1/2 transform -translate-y-1/2 mr-2", right: "left-full top-1/2 transform -translate-y-1/2 ml-2", "top-left": "bottom-full right-0 mb-2", "top-right": "bottom-full left-0 mb-2", "bottom-left": "top-full right-0 mt-2", "bottom-right": "top-full left-0 mt-2", }; return positions[actualPosition] || positions.top; };
const getArrowStyles = () => { if (!arrow) return "";
const arrows = { top: "absolute top-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900", bottom: "absolute bottom-full left-1/2 transform -translate-x-1/2 border-l-4 border-r-4 border-b-4 border-transparent border-b-gray-900", left: "absolute left-full top-1/2 transform -translate-y-1/2 border-t-4 border-b-4 border-l-4 border-transparent border-l-gray-900", right: "absolute right-full top-1/2 transform -translate-y-1/2 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900", "top-left": "absolute top-full right-2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900", "top-right": "absolute top-full left-2 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900", "bottom-left": "absolute bottom-full right-2 border-l-4 border-r-4 border-b-4 border-transparent border-b-gray-900", "bottom-right": "absolute bottom-full left-2 border-l-4 border-r-4 border-b-4 border-transparent border-b-gray-900", }; return arrows[actualPosition] || arrows.top; };
const triggerProps = { ref: triggerRef, className: `relative inline-block ${className}`, };
if (trigger === "hover") { triggerProps.onMouseEnter = showTooltip; triggerProps.onMouseLeave = hideTooltip; triggerProps.onFocus = showTooltip; triggerProps.onBlur = hideTooltip; } else if (trigger === "click") { triggerProps.onClick = toggleTooltip; }
return ( <div {...triggerProps}> {children}
{isVisible && content && ( <div ref={tooltipRef} className={` absolute z-50 ${getPositionStyles()} ${maxWidth} px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-lg transition-all duration-200 ease-out pointer-events-none ${contentClassName} `} role="tooltip" aria-hidden={!isVisible} > {content} {arrow && <div className={getArrowStyles()} />} </div> )} </div> );};
export default Tooltip;
Usage Example
import React from "react";import Tooltip from "./Tooltip";import { InformationCircleIcon, TrashIcon, PencilIcon, QuestionMarkCircleIcon,} from "@heroicons/react/24/outline";
const App = () => { return ( <div className="max-w-4xl mx-auto p-6 space-y-8"> <h1 className="text-3xl font-bold text-gray-900">Tooltip Examples</h1>
{/* Basic Usage */} <div className="space-y-6"> <h2 className="text-xl font-semibold">Basic Tooltips</h2> <div className="flex flex-wrap gap-6 items-center"> <Tooltip content="This is a basic tooltip"> <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> Hover me </button> </Tooltip>
<Tooltip content="Click to show/hide" trigger="click"> <button className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"> Click me </button> </Tooltip>
<Tooltip content="This tooltip appears instantly" delay={0}> <span className="text-blue-600 underline cursor-help"> Instant tooltip </span> </Tooltip> </div> </div>
{/* Different Positions */} <div className="space-y-6"> <h2 className="text-xl font-semibold">Positioning</h2> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 p-8"> <Tooltip content="Tooltip on top" position="top"> <button className="px-3 py-2 bg-gray-500 text-white rounded text-sm"> Top </button> </Tooltip>
<Tooltip content="Tooltip on bottom" position="bottom"> <button className="px-3 py-2 bg-gray-500 text-white rounded text-sm"> Bottom </button> </Tooltip>
<Tooltip content="Tooltip on left" position="left"> <button className="px-3 py-2 bg-gray-500 text-white rounded text-sm"> Left </button> </Tooltip>
<Tooltip content="Tooltip on right" position="right"> <button className="px-3 py-2 bg-gray-500 text-white rounded text-sm"> Right </button> </Tooltip> </div> </div>
{/* With Icons */} <div className="space-y-6"> <h2 className="text-xl font-semibold">Icon Tooltips</h2> <div className="flex gap-6 items-center"> <Tooltip content="Get help and documentation"> <button className="p-2 text-blue-600 hover:bg-blue-50 rounded-full"> <InformationCircleIcon className="w-5 h-5" /> </button> </Tooltip>
<Tooltip content="Delete this item permanently" position="bottom"> <button className="p-2 text-red-600 hover:bg-red-50 rounded-full"> <TrashIcon className="w-5 h-5" /> </button> </Tooltip>
<Tooltip content="Edit item details" position="left"> <button className="p-2 text-green-600 hover:bg-green-50 rounded-full"> <PencilIcon className="w-5 h-5" /> </button> </Tooltip>
<Tooltip content="Need help? Click for more information" trigger="click" > <button className="p-2 text-purple-600 hover:bg-purple-50 rounded-full"> <QuestionMarkCircleIcon className="w-5 h-5" /> </button> </Tooltip> </div> </div>
{/* Advanced Examples */} <div className="space-y-6"> <h2 className="text-xl font-semibold">Advanced Usage</h2>
{/* Rich Content Tooltip */} <div className="space-y-4"> <h3 className="text-lg font-medium">Rich Content</h3> <Tooltip content={ <div> <div className="font-semibold mb-1">Pro Tip!</div> <div>Use keyboard shortcuts to work faster:</div> <ul className="mt-2 text-xs space-y-1"> <li>• Ctrl+S to save</li> <li>• Ctrl+Z to undo</li> <li>• Ctrl+C to copy</li> </ul> </div> } maxWidth="max-w-sm" position="bottom" > <span className="inline-flex items-center px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm font-medium cursor-help"> <QuestionMarkCircleIcon className="w-4 h-4 mr-1" /> Keyboard Shortcuts </span> </Tooltip> </div>
{/* No Arrow Example */} <div className="space-y-4"> <h3 className="text-lg font-medium">Without Arrow</h3> <Tooltip content="Clean tooltip without arrow pointer" arrow={false} contentClassName="bg-black" > <button className="px-4 py-2 bg-gray-800 text-white rounded"> No Arrow </button> </Tooltip> </div>
{/* Disabled State */} <div className="space-y-4"> <h3 className="text-lg font-medium">Disabled Tooltip</h3> <Tooltip content="This tooltip won't show" disabled> <button className="px-4 py-2 bg-gray-400 text-gray-600 rounded cursor-not-allowed"> Disabled Tooltip </button> </Tooltip> </div> </div>
{/* Form Example */} <div className="space-y-6"> <h2 className="text-xl font-semibold">Form Integration</h2> <div className="max-w-md space-y-4"> <div> <label className="flex items-center text-sm font-medium text-gray-700 mb-2"> Email Address <Tooltip content="We'll never share your email with anyone else" position="right" > <InformationCircleIcon className="w-4 h-4 ml-1 text-gray-400 cursor-help" /> </Tooltip> </label> <input type="email" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter your email" /> </div>
<div> <label className="flex items-center text-sm font-medium text-gray-700 mb-2"> Password <Tooltip content="Password must be at least 8 characters with uppercase, lowercase, and numbers" position="right" maxWidth="max-w-sm" > <InformationCircleIcon className="w-4 h-4 ml-1 text-gray-400 cursor-help" /> </Tooltip> </label> <input type="password" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter your password" /> </div> </div> </div> </div> );};
export default App;
Props
Prop | Type | Default | Description |
---|---|---|---|
children | ReactNode | required | Element that triggers the tooltip |
content | string | ReactNode | required | Tooltip content |
position | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top' | Tooltip position relative to trigger |
trigger | 'hover' | 'click' | 'hover' | How tooltip is triggered |
delay | number | 500 | Delay in ms before showing (hover only) |
className | string | '' | Additional classes for wrapper |
contentClassName | string | '' | Additional classes for tooltip content |
arrow | boolean | true | Whether to show arrow pointer |
disabled | boolean | false | Disable tooltip functionality |
maxWidth | string | 'max-w-xs' | Maximum width class |
Features
- ✅ Smart Positioning: Automatically adjusts position to stay in viewport
- ✅ Multiple Triggers: Hover or click activation
- ✅ Customizable Delay: Configurable show delay for hover tooltips
- ✅ Rich Content: Support for text, HTML, and React components
- ✅ Smooth Animations: CSS transitions for professional appearance
- ✅ Accessibility: ARIA attributes and keyboard navigation
- ✅ Responsive: Works across all screen sizes
- ✅ Arrow Pointer: Optional arrow pointing to trigger element
Position Options
Basic Positions
- top: Above the element (default)
- bottom: Below the element
- left: To the left of the element
- right: To the right of the element
Corner Positions
- top-left: Above and aligned to left edge
- top-right: Above and aligned to right edge
- bottom-left: Below and aligned to left edge
- bottom-right: Below and aligned to right edge
Common Patterns
Icon Tooltips
<Tooltip content="Add new item"> <button className="p-2 text-blue-600 hover:bg-blue-50 rounded"> <PlusIcon className="w-5 h-5" /> </button></Tooltip>
Form Field Help
<label className="flex items-center"> Username <Tooltip content="Username must be 3-20 characters" position="right"> <InformationCircleIcon className="w-4 h-4 ml-1 text-gray-400" /> </Tooltip></label>
Click Tooltips for Mobile
<Tooltip content="Detailed information here" trigger="click"> <span className="text-blue-600 underline cursor-pointer">Learn more</span></Tooltip>
Rich Content Tooltips
<Tooltip content={ <div> <div className="font-semibold">Feature Preview</div> <div className="text-xs mt-1">Available in Pro plan</div> </div> } maxWidth="max-w-sm"> <button>Premium Feature</button></Tooltip>
Advanced Usage
Custom Styling
// Dark theme tooltip<Tooltip content="Dark themed tooltip" contentClassName="bg-black text-white border border-gray-700"> <button>Dark Tooltip</button></Tooltip>
// Colorful tooltip<Tooltip content="Colorful notification" contentClassName="bg-gradient-to-r from-purple-500 to-pink-500 text-white" arrow={false}> <button>Gradient Tooltip</button></Tooltip>
Controlled Tooltip
const [showTooltip, setShowTooltip] = useState(false);
<Tooltip content="Controlled tooltip" trigger="click" isVisible={showTooltip} onVisibilityChange={setShowTooltip}> <button>Controlled</button></Tooltip>;
Long Content with Wrapping
<Tooltip content="This is a very long tooltip message that will wrap to multiple lines and demonstrate the max width functionality" maxWidth="max-w-md" position="bottom"> <span className="cursor-help underline">Long tooltip example</span></Tooltip>
Trigger Types
Hover (Default)
- Shows on mouse enter and focus
- Hides on mouse leave and blur
- Configurable delay for better UX
Click
- Toggles on click
- Good for mobile devices
- Stays open until clicked again or focus lost
Positioning Logic
The tooltip automatically adjusts its position when:
- Top tooltip would overflow viewport top → switches to bottom
- Bottom tooltip would overflow viewport bottom → switches to top
- Left tooltip would overflow viewport left → switches to right
- Right tooltip would overflow viewport right → switches to left
Installation
Install Heroicons for icon examples:
npm install @heroicons/react
Best Practices
- Keep content concise - Tooltips should provide quick context
- Use appropriate triggers - Hover for desktop, click for mobile-friendly
- Position wisely - Consider tooltip content length and screen space
- Accessibility first - Ensure tooltips are keyboard accessible
- Don’t overuse - Not every element needs a tooltip
- Test on mobile - Ensure tooltips work well on touch devices
- Provide escape routes - Always allow users to dismiss tooltips
Accessibility Features
- ARIA Roles: Proper
tooltip
role for screen readers - Keyboard Support: Works with Tab navigation and focus states
- Screen Reader Friendly: Content is announced when tooltip appears
- Focus Management: Maintains focus on trigger element
- High Contrast: Dark background ensures good contrast
Animation Details
- Entry: Smooth fade-in with slight scale animation
- Exit: Fade-out transition on hide
- Duration: 200ms for snappy, professional feel
- Easing:
ease-out
for natural motion
Notes
- Tooltips automatically hide when trigger loses focus
- Smart positioning prevents tooltips from going off-screen
- Arrow pointer adjusts based on actual position
- No external dependencies except Heroicons for icon examples
- Works with both controlled and uncontrolled patterns