HextaUIHextaUI
/Components/Animation

Hover Link Preview

When a user hovers over a link, a preview of the link's destination appears.

Preview

Hey, have you triedHextaUI?It's amazing!

(Try hovering link)

This component is available on 21st.dev.

Installation

HoverLinkPreview.tsx
"use client";

import React, { useRef, useState } from "react";
import {
  motion,
  AnimatePresence,
  useMotionValue,
  useSpring,
} from "framer-motion";

interface HoverLinkPreviewProps {
  href: string;
  previewImage: string;
  imageAlt?: string;
  children: React.ReactNode;
}

const HoverLinkPreview: React.FC<HoverLinkPreviewProps> = ({
  href,
  previewImage,
  imageAlt = "Link preview",
  children,
}) => {
  const [showPreview, setShowPreview] = useState(false);
  const prevX = useRef<number | null>(null);

  // Motion values for smooth animation
  const motionTop = useMotionValue(0);
  const motionLeft = useMotionValue(0);
  const motionRotate = useMotionValue(0);

  // Springs for natural movement
  const springTop = useSpring(motionTop, { stiffness: 300, damping: 30 });
  const springLeft = useSpring(motionLeft, { stiffness: 300, damping: 30 });
  const springRotate = useSpring(motionRotate, { stiffness: 300, damping: 20 });

  // Handlers
  const handleMouseEnter = () => {
    setShowPreview(true);
    prevX.current = null;
  };

  const handleMouseLeave = () => {
    setShowPreview(false);
    prevX.current = null;
    motionRotate.set(0);
  };

  const handleMouseMove = (e: React.MouseEvent<HTMLAnchorElement>) => {
    const PREVIEW_WIDTH = 192;
    const PREVIEW_HEIGHT = 112;
    const OFFSET_Y = 40;

    // Position the preview
    motionTop.set(e.clientY - PREVIEW_HEIGHT - OFFSET_Y);
    motionLeft.set(e.clientX - PREVIEW_WIDTH / 2);

    // Calculate tilt based on horizontal movement
    if (prevX.current !== null) {
      const deltaX = e.clientX - prevX.current;
      const newRotate = Math.max(-15, Math.min(15, deltaX * 1.2));
      motionRotate.set(newRotate);
    }
    prevX.current = e.clientX;
  };

  return (
    <>
      <a
        href={href}
        target="_blank"
        rel="noopener noreferrer"
        className="relative inline-block cursor-pointer text-blue-600 underline"
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        onMouseMove={handleMouseMove}
      >
        {children}
      </a>

      <AnimatePresence>
        {showPreview && (
          <motion.div
            initial={{ opacity: 0, scale: 0.8, y: -10, rotate: 0 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.8, y: -10, rotate: 0 }}
            style={{
              position: "fixed",
              top: springTop,
              left: springLeft,
              rotate: springRotate,
              zIndex: 50,
              pointerEvents: "none",
            }}
          >
            <div className="bg-white border rounded-2xl shadow-lg p-2 min-w-[180px] max-w-xs">
              <img
                src={previewImage}
                alt={imageAlt}
                draggable={false}
                className="w-48 h-28 object-cover rounded-md"
              />
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
};

export { HoverLinkPreview };

Usage

App.tsx
import { HoverLinkPreview } from "@/components/ui/HoverLinkPreview";

const HoverLinkPreviewExample: React.FC = () => (
  <div className="flex flex-col gap-12 items-center text-center">
    <div className="p-10 flex gap-1">
      Hey, have you tried
      <HoverLinkPreview
        href="https://hextaui.com"
        previewImage="/banner.png"
        imageAlt="Example preview"
      >
        HextaUI?
      </HoverLinkPreview>
      It's amazing!
    </div>

    <p>(Try hovering link)</p>
  </div>
);
Edit on GitHub

Last updated on