DEV Community

Cover image for Animated Video Tooltip using typescript and framer-motion
Bro Karim
Bro Karim

Posted on

Animated Video Tooltip using typescript and framer-motion

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>
  );
};

Enter fullscreen mode Exit fullscreen mode

Top comments (0)