Hover Facing Card

A card with a color gradient that tilts, and the text pops up when hovered over.

Share Your Vision

Tell us about your business goals and the website you've always dreamed of. We'll collaborate to bring your ideas to life.

Plan Your Trip

Plan and build out your dream vacation to Mount Elbrus with the Trip Explor.

Installation

Install Dependencies

npm i clsx tailwind-merge framer-motion

Create @/utils/cn.ts file

import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Hover Facing Card Component

interface HoverFacingCardProps extends React.HTMLAttributes<HTMLDivElement> {
  onClick?: () => void;
}

export const HoverFacingCard: React.FC<HoverFacingCardProps> = ({ children, className, onClick, ...props }) => {
  const cardRef: RefObject<HTMLDivElement> = useRef(null);
  const [x, setX] = useState(50);
  const [y, setY] = useState(50);

  const reset = (card: any) => {
    setX(50);
    setY(50);

    if (card) {
      card.setAttribute("data-x", "50");
      card.setAttribute("data-y", "50");
      card.style.setProperty("--x", "50");
      card.style.setProperty("--y", "50");
    }
  };

  const handleMouseMove = (
    e: { clientX: number; clientY: number },
    data: { top: number; left: number; bottom: number; right: number; width: number; height: number },
    card: any
  ) => {
    if (
      card &&
      e.clientX + 20 >= data.left &&
      e.clientX - 20 <= data.right &&
      e.clientY + 20 >= data.top &&
      e.clientY - 20 <= data.bottom
    ) {
      const newX = Math.round((100 / data.width) * (e.clientX - data.left));
      const newY = Math.round((100 / data.height) * (e.clientY - data.top));
      setX(newX);
      setY(newY);
      card.setAttribute("data-x", newX.toString());
      card.setAttribute("data-y", newY.toString());
      card.style.setProperty("--x", newX.toString());
      card.style.setProperty("--y", newY.toString());
    } else {
      reset(card);
    }
  };

  useEffect(() => {
    const card = cardRef.current;

    if (card) {
      const rect = card.getBoundingClientRect();
      const data = {
        top: rect.top,
        left: rect.left,
        bottom: rect.top + rect.height,
        right: rect.left + rect.width,
        width: rect.width,
        height: rect.height
      };

      const handleMouseOut = () => reset(card);
      const handleMouseMoveWithParams = (e: any) => handleMouseMove(e, data, card);

      card.addEventListener("mousemove", handleMouseMoveWithParams);
      card.addEventListener("mouseout", handleMouseOut);

      return () => {
        card.removeEventListener("mousemove", handleMouseMoveWithParams);
        card.removeEventListener("mouseout", handleMouseOut);
      };
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <article
      ref={cardRef}
      className={cn(style.card, className)}
      onClick={onClick}
      style={{ ["--x" as any]: x, ["--y" as any]: y }}
      data-x={x}
      data-y={y}
      {...props}
    >
      {children}
    </article>
  );
};

interface HoverFacingCardTitleProps extends React.HTMLAttributes<HTMLDivElement> {}

export const HoverFacingCardTitle: React.FC<HoverFacingCardTitleProps> = ({ children, className, ...props }) => {
  return (
    <h2 data-content={children} className={cn(style.content, className)} {...props}>
      {children}
    </h2>
  );
};

interface HoverFacingCardDescriptionProps extends React.HTMLAttributes<HTMLDivElement> {}

export const HoverFacingCardDescription: React.FC<HoverFacingCardDescriptionProps> = ({ children, className, ...props }) => {
  return (
    <p data-content={children} className={cn(style.content, className)} {...props}>
      {children}
    </p>
  );
};