Video Tooltip is an animated component that activates when users hover over an avatar.
This component displays a short video of the person introducing themselves or providing additional context, adding a personal and interactive touch.
It's particularly useful for creating memorable user experiences, offering quick insights about team members, speakers, or influencers without requiring extra clicks.
Demo
Source Code
video-tooltip.tsx
import { useState, useCallback, useMemo } from "react";
import { motion, useTransform, AnimatePresence, useMotionValue, useSpring } from "framer-motion";
import { cn } from "@/lib/utils";
interface TooltipItem {
id: number;
name: string;
designation: string;
image: string;
video: string;
text: string;
}
interface VideoTooltipProps {
items: TooltipItem[];
className?: string;
}
export const VideoTooltip = ({ items, className = "" }: VideoTooltipProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [showText, setShowText] = useState(false);
const springConfig = useMemo(
() => ({
stiffness: 100,
damping: 5,
}),
[]
);
// Motion setup
const x = useMotionValue(0);
const translateX = useSpring(useTransform(x, [-100, 100], [-50, 50]), springConfig);
// Optimize event handler with useCallback
const handleMouseMove = useCallback(
(event: React.MouseEvent<HTMLElement>) => {
const halfWidth = event.currentTarget.offsetWidth / 2;
x.set(event.nativeEvent.offsetX - halfWidth);
},
[x]
);
return (
<div className={cn("flex items-center gap-2", className)}>
{items.map((item) => (
<div className="-mr-4 relative group" key={item.name} onMouseEnter={() => setHoveredIndex(item.id)} onMouseLeave={() => setHoveredIndex(null)}>
<AnimatePresence mode="popLayout">
{hoveredIndex === item.id && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.6 }}
animate={{
opacity: 1,
y: 0,
scale: 1,
transition: {
stiffness: 260,
damping: 10,
duration: 0.3,
},
width: showText ? "300px" : "96px",
height: showText ? "auto" : "96px",
}}
exit={{ opacity: 0, y: 20, scale: 0.6 }}
style={{
translateX: translateX,
// rotate: rotate,
// whiteSpace: "nowrap",
}}
className="absolute w-24 h-24 group -top-28 -left-1/2 translate-x-1/2 border-2 border-white flex text-xs flex-col bg-white items-center justify-center rounded-md z-50 shadow-xl px-4 py-2"
>
<motion.div animate={{ opacity: showText ? 0 : 1 }} transition={{ duration: 0.3 }} className="absolute inset-0 z-10">
<video src={item.video} autoPlay muted loop playsInline className="w-full h-full object-cover rounded-md ring-black" />
</motion.div>
<motion.div className="p-1 w-full bg-white max-h-32 overflow-y-auto flex flex-col" initial={{ opacity: 0 }} animate={{ opacity: showText ? 1 : 0 }} transition={{ duration: 0.3 }}>
<p className="text-sm text-black text-foreground-foreground">{item.text}</p>
</motion.div>
<div className=" relative h-full w-full ">
<div className={`absolute ${showText ? "left-0" : "-left-[16%]"} bottom-2 flex space-x-2 z-30 items-center justify-center rounded-full border border-white text-black p-1`}>
<button className={`text-[8px] h-auto rounded-full px-1 ${!showText ? "bg-black text-white" : ""}`} onClick={() => setShowText(false)}>
Video
</button>
<button className={`text-[8px] h-auto rounded-full px-1 ${showText ? "bg-black text-white" : ""}`} onClick={() => setShowText(true)}>
Text
</button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<img
onMouseMove={handleMouseMove}
height={100}
width={100}
src={item.image}
alt={item.name}
className="object-cover !m-0 !p-0 object-top rounded-full h-14 w-14 border-2 group-hover:scale-105 group-hover:z-30 border-background relative transition duration-500"
/>
</div>
))}
</div>
);
};
Top comments (0)