DEV Community

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

Posted on • Edited on

2

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)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up