Back

Card Hover Effect

Install dependencies
npm i framer-motion clsx tailwind-merge
utils/cn.ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
CardHoverEffect.tsx
// @NOTE: in case you are using Next.js
"use client";

import { useState } from "react";

import { AnimatePresence, motion } from "framer-motion";

import { cn } from "@/utils/cn";

type CardHoverEffectProps = {
  containerClassName?: string;
  itemClassName?: string;
  hoveredItemClassName?: string;
};

export function CardHoverEffect({
  containerClassName,
  itemClassName,
  hoveredItemClassName,
}: CardHoverEffectProps) {
  const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);

  const items = [
    {
      title: "Luxe",
      description:
        "Explore the new website that simplifies the creation of sophisticated dark mode components.",
      href: "https://luxe.guhrodrigues.com",
    },
    {
      title: "Luxe",
      description:
        "Explore the new website that simplifies the creation of sophisticated dark mode components.",
      href: "https://luxe.guhrodrigues.com",
    },
    {
      title: "Luxe",
      description:
        "Explore the new website that simplifies the creation of sophisticated dark mode components.",
      href: "https://luxe.guhrodrigues.com",
    },
  ];

  return (
    <div className={cn("grid md:grid-cols-3", containerClassName)}>
      {items.map((item, idx) => {
        const { title, description, href } = item;

        return (
          <a
            key={idx}
            href={href}
            target="_blank"
            rel="noopener noreferrer"
            className={cn("relative flex flex-col gap-3 p-4", itemClassName)}
            onMouseEnter={() => setHoveredIdx(idx)}
            onMouseLeave={() => setHoveredIdx(null)}
          >
            <AnimatePresence>
              {hoveredIdx === idx && (
                <motion.span
                  className={cn(
                    "absolute inset-0 z-0 block h-full w-full rounded-xl bg-neutral-900",
                    hoveredItemClassName
                  )}
                  layoutId="cardHoverEffect"
                  initial={{ opacity: 0 }}
                  animate={{
                    opacity: 1,
                    transition: { duration: 0.15 },
                  }}
                  exit={{
                    opacity: 0,
                    transition: { duration: 0.15, delay: 0.2 },
                  }}
                />
              )}
            </AnimatePresence>
            <div className="z-[1] space-y-3">
              <h1 className="font-medium text-white">{title}</h1>
              <p className="text-neutral-400">{description}</p>
            </div>
          </a>
        );
      })}
    </div>
  );
}