Animated Tooltip
Animated Tooltip é um componente de sobreposição para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
SmoothUI
MIT
overlays
O que é
Animated Tooltip é um componente de sobreposição para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion
Licença
Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Animated Tooltip
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import type { ReactNode } from "react";
import { useCallback, useEffect, useId, useRef, useState } from "react";
export type AnimatedTooltipPlacement = "top" | "bottom" | "left" | "right";
export interface AnimatedTooltipProps {
/** The trigger element the tooltip is anchored to */
children: ReactNode;
/** Additional CSS class names for the tooltip container */
className?: string;
/** Content displayed inside the tooltip */
content: ReactNode;
/** Delay in milliseconds before the tooltip appears */
delay?: number;
/** Placement of the tooltip relative to the trigger */
placement?: AnimatedTooltipPlacement;
}
const SPRING = {
bounce: 0.1,
duration: 0.25,
type: "spring" as const,
};
const placementStyles: Record<AnimatedTooltipPlacement, string> = {
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",
top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
};
const arrowStyles: Record<AnimatedTooltipPlacement, string> = {
bottom:
"bottom-full left-1/2 -translate-x-1/2 border-b-foreground border-x-transparent border-t-transparent",
left: "left-full top-1/2 -translate-y-1/2 border-l-foreground border-y-transparent border-r-transparent",
right:
"right-full top-1/2 -translate-y-1/2 border-r-foreground border-y-transparent border-l-transparent",
top: "top-full left-1/2 -translate-x-1/2 border-t-foreground border-x-transparent border-b-transparent",
};
const arrowBorderSize: Record<AnimatedTooltipPlacement, string> = {
bottom: "border-4",
left: "border-4",
right: "border-4",
top: "border-4",
};
const getInitialTransform = (
placement: AnimatedTooltipPlacement
): { opacity: number; scale: number; x: number; y: number } => {
const base = { opacity: 0, scale: 0.95, x: 0, y: 0 };
switch (placement) {
case "top":
return { ...base, y: 4 };
case "bottom":
return { ...base, y: -4 };
case "left":
return { ...base, x: 4 };
case "right":
return { ...base, x: -4 };
}
};
const AnimatedTooltip = ({
content,
placement = "top",
delay = 0,
children,
className,
}: AnimatedTooltipProps) => {
const shouldReduceMotion = useReducedMotion();
const [isVisible, setIsVisible] = useState(false);
const [isHoverDevice, setIsHoverDevice] = useState(false);
const tooltipId = useId();
const delayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const mediaQuery = window.matchMedia("(hover: hover) and (pointer: fine)");
setIsHoverDevice(mediaQuery.matches);
const handleChange = (e: MediaQueryListEvent) => {
setIsHoverDevice(e.matches);
};
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, []);
const show = useCallback(() => {
if (delay > 0) {
delayTimerRef.current = setTimeout(() => {
setIsVisible(true);
}, delay);
} else {
setIsVisible(true);
}
}, [delay]);
const hide = useCallback(() => {
if (delayTimerRef.current !== null) {
clearTimeout(delayTimerRef.current);
delayTimerRef.current = null;
}
setIsVisible(false);
}, []);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent) => {
if (event.key === "Escape") {
hide();
}
},
[hide]
);
const initialTransform = getInitialTransform(placement);
return (
<span
className="relative inline-flex"
onBlur={hide}
onFocus={show}
onKeyDown={handleKeyDown}
onMouseEnter={isHoverDevice ? show : undefined}
onMouseLeave={isHoverDevice ? hide : undefined}
>
<span aria-describedby={isVisible ? tooltipId : undefined}>
{children}
</span>
<AnimatePresence>
{isVisible ? (
<motion.span
animate={
shouldReduceMotion
? { opacity: 1 }
: { opacity: 1, scale: 1, x: 0, y: 0 }
}
className={cn(
"absolute z-50 w-max max-w-xs rounded-md bg-foreground px-3 py-1.5 text-background text-sm shadow-md",
placementStyles[placement],
className
)}
exit={
shouldReduceMotion
? { opacity: 0, transition: { duration: 0 } }
: {
...initialTransform,
transition: { duration: 0.15 },
}
}
id={tooltipId}
initial={shouldReduceMotion ? { opacity: 0 } : initialTransform}
role="tooltip"
transition={shouldReduceMotion ? { duration: 0 } : SPRING}
>
{content}
<span
aria-hidden="true"
className={cn(
"absolute block h-0 w-0",
arrowBorderSize[placement],
arrowStyles[placement]
)}
/>
</motion.span>
) : null}
</AnimatePresence>
</span>
);
};
export default AnimatedTooltip;