Mask Card

An interactive masked hover card that uses SVG masking to create a unique folder-shaped frame. It features a dual-layer design where a stylized overlay slides down on hover to reveal underlying content, powered by Framer Motion for smooth, high-performance animations.

framer-motion

No mercy.

I’m not here to be liked. I’m here to get it done. - Butcher

The Boys

Oi, Some words by Billy Butcher

Installation

Install the required dependencies:

npm install framer-motion
Link to required files
Follow this link and download the required files. - https://github.com/railav61/data/tree/main/forkui-data/mask-card-data
components/ui/mask-card.tsx
"use client";

import React from "react";
import { cn } from "@/lib/utils";
import { motion, Variants } from "framer-motion";

interface CardProps {
  backTitle?: string;
  backDesc?: string;
  backBg?: string;
  overlayBg?: string;
  overlayTitle?: string;
  overlayDesc?: string;
  className?: string;
  rotate?: boolean;
}

const overlayVariants: Variants = {
  rest: { y: 0 },
  hover: { y: "100%" },
};

const maskStyles = (url: string): React.CSSProperties => ({
  maskImage: `url(${url})`,
  WebkitMaskImage: `url(${url})`,
  maskRepeat: "no-repeat",
  WebkitMaskRepeat: "no-repeat",
  maskPosition: "center",
  WebkitMaskPosition: "center",
  maskSize: "contain",
  WebkitMaskSize: "contain",
});

export default function MaskCard({
  backBg = "https://res.cloudinary.com/dmqwpwo6c/image/upload/v1776147231/boys-2_nw4bja.jpg",
  backDesc = "I’m not here to be liked. I’m here to get it done. - Butcher",
  backTitle = "No mercy.",
  overlayBg = "/bg-ad.svg",
  overlayTitle = "The Boys",
  overlayDesc = "Oi, Some words by Billy Butcher",
  className,
  rotate = false,
}: CardProps) {
  return (
    <motion.div
      className={cn(
        "relative h-72 w-72 mt-1",
        rotate && "rotate-180",
        className,
      )}
      style={maskStyles("/folder-svg.svg")}
      role="region"
      aria-label={overlayTitle}
    >
      <motion.div
        className={cn(
          "relative w-full h-full overflow-hidden",
          rotate && "rotate-180", // Counter-rotate content so text stays upright
        )}
        whileHover="hover"
        initial="rest"
      >
        {/* Underlay Content (Visible on Hover) */}
        <div
          className="absolute inset-0 p-5 z-[1] text-white flex flex-col justify-between"
          style={{
            backgroundImage: `url(${backBg})`,
            backgroundSize: "cover",
            backgroundPosition: "center",
          }}
        >
          <h2 className="text-right font-extrabold text-2xl drop-shadow-md">
            {backTitle}
          </h2>
          <p className="w-full font-bold text-xl drop-shadow-md">{backDesc}</p>
        </div>

        {/* Overlay Content (Slides Down) */}
        <motion.div
          className="absolute inset-0 z-[2] p-5 flex flex-col justify-end bg-no-repeat"
          variants={overlayVariants}
          transition={{
            duration: 0.5,
            ease: [0.33, 1, 0.68, 1], // Custom "out-expo" for smoother feel
          }}
          style={{
            backgroundImage: `url(${overlayBg})`,
            backgroundSize: "cover",
            backgroundPosition: "center",
          }}
        >
          <h2 className="font-extrabold text-5xl text-white mb-2 leading-tight">
            {overlayTitle}
          </h2>
          <p className="text-white font-bold text-lg">{overlayDesc}</p>
        </motion.div>
      </motion.div>
    </motion.div>
  );
}