A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.
1'use client';23import { Button } from '@/components/ui/button';4import { Tooltip } from '@/components/ui/tooltip';56export function Default() {7 return (8 <Tooltip>9 <Tooltip.Trigger asChild>10 <Button variant="outline">Hover me</Button>11 </Tooltip.Trigger>12 <Tooltip.Content>13 <p>Add to library</p>14 </Tooltip.Content>15 </Tooltip>16 );17}
Installation
Copy and paste the following code in your project.
'use client';import { AnimatePresence, HTMLMotionProps, motion } from 'motion/react';import * as React from 'react';import { cn } from '../lib/cn';// --- Animation constants (module level) ---const TOOLTIP_POSITION_CLASSES = {top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',left: 'right-full top-1/2 -translate-y-1/2 mr-2',right: 'left-full top-1/2 -translate-y-1/2 ml-2',} as const;const TOOLTIP_ANIMATION_VARIANTS = {top: {initial: { opacity: 0, y: 5, filter: 'blur(4px)' },animate: { opacity: 1, y: 0, filter: 'blur(0px)' },},bottom: {initial: { opacity: 0, y: -5, filter: 'blur(4px)' },animate: { opacity: 1, y: 0, filter: 'blur(0px)' },},left: {initial: { opacity: 0, x: 5, filter: 'blur(4px)' },animate: { opacity: 1, x: 0, filter: 'blur(0px)' },},right: {initial: { opacity: 0, x: -5, filter: 'blur(4px)' },animate: { opacity: 1, x: 0, filter: 'blur(0px)' },},} as const;const TOOLTIP_ARROW_POSITION = {top: 'bottom-[-4px] left-1/2 -translate-x-1/2 border-b-0 border-r-0',bottom: 'top-[-4px] left-1/2 -translate-x-1/2 border-t-0 border-l-0',left: 'right-[-4px] top-1/2 -translate-y-1/2 border-l-0 border-b-0',right: 'left-[-4px] top-1/2 -translate-y-1/2 border-r-0 border-t-0',} as const;const TOOLTIP_TRANSITION = { duration: 0.2, ease: 'easeOut' } as const;const TOOLTIP_STYLE = { willChange: 'opacity, transform, filter' } as const;// --- Context ---interface TooltipContextType {open: boolean;setOpen: (open: boolean) => void;delayDuration: number;id: string;}const TooltipContext = React.createContext<TooltipContextType | undefined>(undefined);const useTooltip = () => {const context = React.use(TooltipContext);if (!context) {throw new Error('useTooltip must be used within a TooltipProvider');}return context;};// --- Components ---interface TooltipProps {children: React.ReactNode;delayDuration?: number;open?: boolean;onOpenChange?: (open: boolean) => void;}const TooltipRoot = ({children,delayDuration = 200,open: controlledOpen,onOpenChange,}: TooltipProps) => {const [internalOpen, setInternalOpen] = React.useState(false);const isControlled = controlledOpen !== undefined;const open = isControlled ? controlledOpen : internalOpen;const setOpen = React.useCallback((newState: boolean) => {if (isControlled) {onOpenChange?.(newState);} else {setInternalOpen(newState);}},[isControlled, onOpenChange],);const id = React.useId();return (<TooltipContext value={{ open, setOpen, delayDuration, id }}><div className="relative flex h-fit w-fit items-center justify-center">{children}</div></TooltipContext>);};interface TooltipTriggerProps extends React.HTMLAttributes<HTMLElement> {asChild?: boolean;}function TooltipTrigger({ children, asChild = false, className, ...props }: TooltipTriggerProps) {const { setOpen, delayDuration, id } = useTooltip();const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);const handleMouseEnter = () => {timeoutRef.current = setTimeout(() => {setOpen(true);}, delayDuration);};const handleMouseLeave = () => {if (timeoutRef.current) clearTimeout(timeoutRef.current);setOpen(false);};const handleFocus = () => {setOpen(true);};const handleBlur = () => {setOpen(false);};if (asChild && React.isValidElement(children)) {const child = children as React.ReactElement<any>;return React.cloneElement(child, {...props,'aria-describedby': id,onMouseEnter: (e: React.MouseEvent) => {handleMouseEnter();child.props.onMouseEnter?.(e);},onMouseLeave: (e: React.MouseEvent) => {handleMouseLeave();child.props.onMouseLeave?.(e);},onFocus: (e: React.FocusEvent) => {handleFocus();child.props.onFocus?.(e);},onBlur: (e: React.FocusEvent) => {handleBlur();child.props.onBlur?.(e);},className: cn(className, child.props.className),});}return (<divaria-describedby={id}className={cn('cursor-pointer', className)}onMouseEnter={handleMouseEnter}onMouseLeave={handleMouseLeave}onFocus={handleFocus}onBlur={handleBlur}{...props}>{children}</div>);}interface TooltipContentProps extends HTMLMotionProps<'div'> {side?: 'top' | 'bottom' | 'left' | 'right';sideOffset?: number;children?: React.ReactNode;}const TooltipContent = ({side = 'top',sideOffset = 4,className,children,...props}: TooltipContentProps) => {const { open, id } = useTooltip();const sideOffsetStyle = React.useMemo(() => ({...(side === 'top' && { marginBottom: sideOffset }),...(side === 'bottom' && { marginTop: sideOffset }),...(side === 'left' && { marginRight: sideOffset }),...(side === 'right' && { marginLeft: sideOffset }),...TOOLTIP_STYLE,}),[side, sideOffset],);return (<AnimatePresence>{open && (<motion.divid={id}role="tooltip"initial={TOOLTIP_ANIMATION_VARIANTS[side].initial}animate={TOOLTIP_ANIMATION_VARIANTS[side].animate}exit={TOOLTIP_ANIMATION_VARIANTS[side].initial}transition={TOOLTIP_TRANSITION}style={sideOffsetStyle}className={cn('bg-foreground text-background absolute z-50 rounded-xl px-3 py-1.5 text-xs whitespace-nowrap shadow-md',TOOLTIP_POSITION_CLASSES[side],className,)}{...props}>{children}<divclassName={cn('bg-foreground absolute h-2 w-2 rotate-45', TOOLTIP_ARROW_POSITION[side])}/></motion.div>)}</AnimatePresence>);};const Tooltip = Object.assign(TooltipRoot, {Trigger: TooltipTrigger,Content: TooltipContent,});const TooltipProvider = Tooltip;export { Tooltip, TooltipProvider };
Make sure to update the import paths according to your project structure.
Anatomy
import { Tooltip } from '@/components/ui/tooltip';
<Tooltip><Tooltip.Trigger asChild><Button variant="outline">Hover me</Button></Tooltip.Trigger><Tooltip.Content><p>Helpful tooltip text</p></Tooltip.Content></Tooltip>
Features
- Multiple positions - Top, bottom, left, or right placement
- Delay control - Configurable hover delay
- Smooth animations - Fade and blur entrance effects
- Arrow indicator - Visual pointer to the trigger element
API Reference
Tooltip
Root component that manages tooltip state.
| Prop | Type | Default | Description |
|---|---|---|---|
delayDuration | number | 200 | Delay before showing (ms) |
open | boolean | - | Controlled open state |
onOpenChange | (open: boolean) => void | - | Callback when state changes |
TooltipTrigger
Element that triggers the tooltip on hover.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as child element |
className | string | - | Additional CSS classes |
TooltipContent
The tooltip content with positioning and animations.
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'top' | 'bottom' | 'left' | 'right' | 'top' | Tooltip position |
sideOffset | number | 4 | Distance from trigger (px) |
className | string | - | Additional CSS classes |
Examples
Different Positions
// Top (default)<Tooltip><Tooltip.Trigger asChild><Button>Top</Button></Tooltip.Trigger><Tooltip.Content side="top"><p>Tooltip on top</p></Tooltip.Content></Tooltip>// Bottom<Tooltip><Tooltip.Trigger asChild><Button>Bottom</Button></Tooltip.Trigger><Tooltip.Content side="bottom"><p>Tooltip on bottom</p></Tooltip.Content></Tooltip>
Custom Delay
<Tooltip delayDuration={500}><Tooltip.Trigger asChild><Button>Slow tooltip</Button></Tooltip.Trigger><Tooltip.Content><p>This appears after 500ms</p></Tooltip.Content></Tooltip>