Un popup que muestra información relacionada con un elemento cuando el elemento recibe el foco del teclado o el mouse pasa sobre él.
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}
Instalación
Copia y pega el siguiente código en tu proyecto.
'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 };
Asegúrate de actualizar las rutas de importación según la estructura de tu proyecto.
Anatomía
import { Tooltip } from '@/components/ui/tooltip';
<Tooltip><Tooltip.Trigger asChild><Button variant="outline">Pasa el mouse</Button></Tooltip.Trigger><Tooltip.Content><p>Texto de tooltip útil</p></Tooltip.Content></Tooltip>
Características
- Múltiples posiciones - Colocación arriba, abajo, izquierda o derecha
- Control de retraso - Retraso de hover configurable
- Animaciones suaves - Efectos de entrada con desvanecimiento y desenfoque
- Indicador de flecha - Puntero visual al elemento activador
Referencia de API
Tooltip
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
delayDuration | number | 200 | Retraso antes de mostrar (ms) |
open | boolean | - | Estado abierto controlado |
onOpenChange | (open: boolean) => void | - | Callback cuando cambia el estado |
Tooltip.Trigger
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
asChild | boolean | false | Renderizar como elemento hijo |
className | string | - | Clases CSS adicionales |
Tooltip.Content
| Prop | Tipo | Por defecto | Descripción |
|---|---|---|---|
side | 'top' | 'bottom' | 'left' | 'right' | 'top' | Posición del tooltip |
sideOffset | number | 4 | Distancia del activador (px) |
className | string | - | Clases CSS adicionales |
Ejemplos
Diferentes Posiciones
// Arriba (por defecto)<Tooltip><Tooltip.Trigger asChild><Button>Arriba</Button></Tooltip.Trigger><Tooltip.Content side="top"><p>Tooltip arriba</p></Tooltip.Content></Tooltip>// Abajo<Tooltip><Tooltip.Trigger asChild><Button>Abajo</Button></Tooltip.Trigger><Tooltip.Content side="bottom"><p>Tooltip abajo</p></Tooltip.Content></Tooltip>
Retraso Personalizado
<Tooltip delayDuration={500}><Tooltip.Trigger asChild><Button>Tooltip lento</Button></Tooltip.Trigger><Tooltip.Content><p>Esto aparece después de 500ms</p></Tooltip.Content></Tooltip>