Docs
HoverGlimpse

HoverGlimpse

An elegant interactive menu component with smooth image transitions and magnetic hover effects.

Menu with Bottom Animation

Serene Lake View

Architecture with Magnetic Effect

Modern Minimalism

Installation

Copy and paste the following code into your project.

components/ui/hover-glimpse.tsx

"use client";
 
import React, { useState, useRef } from "react";
import { cn } from "@/lib/utils";
import {
  motion,
  AnimatePresence,
  useSpring,
  useMotionValue,
} from "framer-motion";
 
export interface MenuItem {
  title: string;
  description?: string;
  image: string;
}
 
interface HoverGlimpseProps {
  items: MenuItem[];
  className?: string;
  imageClassName?: string;
  activeItemClassName?: string;
  animate?: "top" | "bottom" | "left" | "right" | "fade";
  magnetic?: boolean;
}
 
export function HoverGlimpse({
  items,
  className,
  imageClassName,
  activeItemClassName,
  animate = "fade",
  magnetic = false,
}: HoverGlimpseProps) {
  const [activeIndex, setActiveIndex] = useState<number>(0);
  const imageRef = useRef<HTMLDivElement>(null);
 
  // Motion values for magnetic effect
  const mouseX = useMotionValue(0);
  const mouseY = useMotionValue(0);
  const springConfig = { damping: 15, stiffness: 150 };
  const springX = useSpring(mouseX, springConfig);
  const springY = useSpring(mouseY, springConfig);
 
  // Handle magnetic effect
  const handleMouseMove = (e: React.MouseEvent) => {
    if (!magnetic || !imageRef.current) return;
 
    const rect = imageRef.current.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;
    const deltaX = e.clientX - centerX;
    const deltaY = e.clientY - centerY;
 
    mouseX.set(deltaX * 0.1);
    mouseY.set(deltaY * 0.1);
  };
 
  const handleMouseLeave = () => {
    if (!magnetic) return;
    mouseX.set(0);
    mouseY.set(0);
  };
 
  const variants = {
    top: {
      initial: {
        opacity: 0,
        y: -50,
        scale: 1.1,
        filter: "blur(8px)",
      },
      animate: {
        opacity: 1,
        y: 0,
        scale: 1,
        filter: "blur(0px)",
      },
      exit: {
        opacity: 0,
        y: 50,
        scale: 0.95,
        filter: "blur(8px)",
      },
    },
    bottom: {
      initial: {
        opacity: 0,
        y: 50,
        scale: 1.1,
        filter: "blur(8px)",
      },
      animate: {
        opacity: 1,
        y: 0,
        scale: 1,
        filter: "blur(0px)",
      },
      exit: {
        opacity: 0,
        y: -50,
        scale: 0.95,
        filter: "blur(8px)",
      },
    },
    left: {
      initial: {
        opacity: 0,
        x: -40,
        scale: 1.1,
        filter: "blur(8px)",
      },
      animate: {
        opacity: 1,
        x: 0,
        scale: 1,
        filter: "blur(0px)",
      },
      exit: {
        opacity: 0,
        x: 50,
        scale: 0.95,
        filter: "blur(8px)",
      },
    },
    right: {
      initial: {
        opacity: 0,
        x: 50,
        scale: 1.1,
        filter: "blur(8px)",
      },
      animate: {
        opacity: 1,
        x: 0,
        scale: 1,
        filter: "blur(0px)",
      },
      exit: {
        opacity: 0,
        x: -50,
        scale: 0.95,
        filter: "blur(8px)",
      },
    },
    fade: {
      initial: {
        opacity: 0,
        scale: 1.2,
        filter: "blur(10px)",
      },
      animate: {
        opacity: 1,
        scale: 1,
        filter: "blur(0px)",
      },
      exit: {
        opacity: 0,
        scale: 0.9,
        filter: "blur(10px)",
      },
    },
  };
 
  const transition = {
    duration: 0.3,
    ease: "easeOut",
  };
 
  return (
    <div
      className={cn("relative flex items-center gap-4", className)}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
    >
      <div className="flex flex-col space-y-1">
        {items.map((item, index) => (
          <motion.button
            key={index}
            className={cn(
              "relative px-4 py-2 text-left text-base transition-colors",
              "hover:text-black dark:hover:text-white",
              activeIndex === index
                ? "text-black dark:text-white " + activeItemClassName
                : "text-neutral-600 dark:text-neutral-400",
            )}
            onHoverStart={() => setActiveIndex(index)}
          >
            <span className="relative z-10 block cursor-pointer">
              <span className="block text-lg font-medium">{item.title}</span>
              {item.description && (
                <span className="block text-sm text-neutral-500 dark:text-neutral-400">
                  {item.description}
                </span>
              )}
            </span>
          </motion.button>
        ))}
      </div>
 
      <motion.div
        ref={imageRef}
        className={cn(
          "relative ml-8 aspect-square w-[300px] overflow-hidden rounded-xl",
          imageClassName,
        )}
        style={
          magnetic
            ? {
                x: springX,
                y: springY,
              }
            : undefined
        }
      >
        <AnimatePresence mode="wait">
          <motion.img
            key={activeIndex}
            src={items[activeIndex].image}
            alt={items[activeIndex].title}
            className="absolute inset-0 h-full w-full rounded-xl object-cover"
            initial={variants[animate].initial}
            animate={variants[animate].animate}
            exit={variants[animate].exit}
            transition={transition}
          />
        </AnimatePresence>
      </motion.div>
    </div>
  );
}

Update the import paths to match your project setup:

import HoverGlimpse from "@/components/ui/hover-glimpse";

Usage

import HoverGlimpse from "@/components/ui/hover-glimpse";
 
const items = [
  {
    title: "Modern Design",
    description: "Clean lines and minimalist approach",
    image: "/images/modern.jpg",
  },
  {
    title: "Natural Beauty",
    description: "Organic forms and materials",
    image: "/images/nature.jpg",
  },
];
 
export default function Example() {
  return <HoverGlimpse items={items} animate="fade" magnetic />;
}

Props

PropTypeDescription
titlestringTitle of the menu item
descriptionstringDescription of the menu item
imagestringImage URL for the menu item

HoverGlimpse Props

PropTypeDefaultDescription
itemsMenuItem[]-Array of menu items to display
classNamestring""Optional custom classes for the container
imageClassNamestring""Optional custom classes for the image container
activeItemClassNamestring""Optional custom classes for the active menu item
animate"top" | "bottom" | "left" | "right" | "fade""fade"Animation direction for image transitions
magneticbooleanfalseEnable magnetic hover effect

Different Animation Directions

Choose from various animation directions.

<HoverGlimpse items={items} animate="right" />
<HoverGlimpse items={items} animate="left" />
<HoverGlimpse items={items} animate="top" />
<HoverGlimpse items={items} animate="bottom" />

Custom Styling

Add custom classes to style the component.

<HoverGlimpse
  items={items}
  className="gap-8"
  imageClassName="rounded-2xl"
  activeItemClassName="font-bold"
/>