3D Card Effect

A card perspective effect — hover over the card to elevate card elements.

3D Card Effect

A card perspective effect: hovering over the card rotates it in 3D space while each CardItem layer lifts independently, elevating elements toward the viewer.

Installation

npm install framer-motion lucide-react react
pnpm install framer-motion lucide-react react
yarn add framer-motion lucide-react react
bun add framer-motion lucide-react react

Copy the 3D Card Component

"use client";import { cn } from "@/lib/utils";import Image from "next/image";import React, {  createContext,  useState,  useContext,  useRef,  useEffect,  useCallback,} from "react";const MouseEnterContext = createContext<  [boolean, React.Dispatch<React.SetStateAction<boolean>>] | undefined>(undefined);export const CardContainer = ({  children,  className,  containerClassName,}: {  children?: React.ReactNode;  className?: string;  containerClassName?: string;}) => {  const containerRef = useRef<HTMLDivElement>(null);  const [isMouseEntered, setIsMouseEntered] = useState(false);  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {    if (!containerRef.current) return;    const { left, top, width, height } =      containerRef.current.getBoundingClientRect();    const x = (e.clientX - left - width / 2) / 25;    const y = (e.clientY - top - height / 2) / 25;    containerRef.current.style.transform = `rotateY(${x}deg) rotateX(${y}deg)`;  };  const handleMouseEnter = () => {    setIsMouseEntered(true);  };  const handleMouseLeave = () => {    if (!containerRef.current) return;    setIsMouseEntered(false);    containerRef.current.style.transform = `rotateY(0deg) rotateX(0deg)`;  };  return (    <MouseEnterContext.Provider value={[isMouseEntered, setIsMouseEntered]}>      <div        className={cn(          "py-20 flex items-center justify-center",          containerClassName        )}        style={{          perspective: "1000px",        }}      >        <div          ref={containerRef}          onMouseEnter={handleMouseEnter}          onMouseMove={handleMouseMove}          onMouseLeave={handleMouseLeave}          className={cn(            "flex items-center justify-center relative transition-all duration-200 ease-linear",            className          )}          style={{            transformStyle: "preserve-3d",          }}        >          {children}        </div>      </div>    </MouseEnterContext.Provider>  );};export const CardBody = ({  children,  className,}: {  children: React.ReactNode;  className?: string;}) => {  return (    <div      className={cn(        "h-96 w-96 [transform-style:preserve-3d]  [&>*]:[transform-style:preserve-3d]",        className      )}    >      {children}    </div>  );};type CardItemProps<T extends React.ElementType = "div"> = {  as?: T;  children?: React.ReactNode;  className?: string;  translateX?: number | string;  translateY?: number | string;  translateZ?: number | string;  rotateX?: number | string;  rotateY?: number | string;  rotateZ?: number | string;} & Omit<React.ComponentPropsWithoutRef<T>, "children" | "className">;export const CardItem = <T extends React.ElementType = "div">({  as,  children,  className,  translateX = 0,  translateY = 0,  translateZ = 0,  rotateX = 0,  rotateY = 0,  rotateZ = 0,  ...rest}: CardItemProps<T>) => {  const ref = useRef<HTMLElement>(null);  const [isMouseEntered] = useMouseEnter();  const handleAnimations = useCallback(() => {    if (!ref.current) return;    if (isMouseEntered) {      ref.current.style.transform = `translateX(${translateX}px) translateY(${translateY}px) translateZ(${translateZ}px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg)`;    } else {      ref.current.style.transform = `translateX(0px) translateY(0px) translateZ(0px) rotateX(0deg) rotateY(0deg) rotateZ(0deg)`;    }  }, [    isMouseEntered,    translateX,    translateY,    translateZ,    rotateX,    rotateY,    rotateZ,  ]);  useEffect(() => {    handleAnimations();  }, [handleAnimations]);  const Tag = (as ?? "div") as React.ElementType;  return (    <Tag      ref={ref as React.Ref<HTMLElement>}      className={cn("w-fit transition duration-200 ease-linear", className)}      {...rest}    >      {children}    </Tag>  );};// Create a hook to use the contextexport const useMouseEnter = () => {  const context = useContext(MouseEnterContext);  if (context === undefined) {    throw new Error("useMouseEnter must be used within a MouseEnterProvider");  }  return context;};export function ThreeDCardDemo() {  return (    <CardContainer>      <CardBody className="relative h-auto w-full rounded-xl border-border bg-card p-6 shadow-theme-sm transition-shadow hover:shadow-theme-md sm:w-[30rem]">        <CardItem          translateZ="50"          className="text-xl font-bold text-foreground"        >          Make things float in air        </CardItem>        <CardItem          as="p"          translateZ="60"          className="mt-2 max-w-sm text-sm text-muted-foreground"        >          Hover over this card to unleash the power of CSS perspective        </CardItem>        <CardItem translateZ="100" className="mt-4 w-full">          <Image            src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"            height="1000"            width="1000"            className="h-60 w-full rounded-xl object-cover group-hover/card:shadow-xl"            alt="thumbnail"          />        </CardItem>        <div className="mt-20 flex items-center justify-between">          <CardItem            translateZ={20}            as="a"            href="https://twitter.com/mannupaaji"            target="_blank"            className="px-4 py-2 text-xs font-normal text-foreground underline-offset-4 hover:underline"          >            Try now →          </CardItem>          <CardItem            translateZ={20}            as="button"            className="rounded-xl bg-primary px-4 py-2 text-xs font-bold text-white transition-colors hover:bg-primary/90"          >            Sign up          </CardItem>        </div>      </CardBody>    </CardContainer>  );}export default ThreeDCardDemo;

Usage

Three primitives are exported — CardContainer, CardBody, and CardItem (plus the useMouseEnter hook for advanced control) — and a ready-to-use ThreeDCardDemo is provided as the default export.

"use client";

import { CardBody, CardContainer, CardItem } from "@/components/mvpblocks/cards/3d-card/3d-card";

export function ThreeDCardDemo() {
  return (
    <CardContainer>
      <CardBody className="relative h-auto w-full rounded-xl border-border bg-card p-6 shadow-theme-sm hover:shadow-theme-md sm:w-[30rem]">
        <CardItem translateZ="50" className="text-xl font-bold text-foreground">
          Make things float in air
        </CardItem>
        <CardItem
          as="p"
          translateZ="60"
          className="mt-2 max-w-sm text-sm text-muted-foreground"
        >
          Hover over this card to unleash the power of CSS perspective
        </CardItem>
        <CardItem translateZ="100" className="mt-4 w-full">
          <img
            src="https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2560&auto=format&fit=crop"
            className="h-60 w-full rounded-xl object-cover"
            alt="thumbnail"
          />
        </CardItem>
        <div className="mt-20 flex items-center justify-between">
          <CardItem
            translateZ={20}
            as="a"
            href="#"
            className="px-4 py-2 text-xs font-normal text-foreground underline-offset-4 hover:underline"
          >
            Try now →
          </CardItem>
          <CardItem
            translateZ={20}
            as="button"
            className="rounded-xl bg-primary px-4 py-2 text-xs font-bold text-white hover:bg-primary/90"
          >
            Sign up
          </CardItem>
        </div>
      </CardBody>
    </CardContainer>
  );
}

Layer your content across multiple CardItem elements and give each one a different translateZ value — higher values appear closer to the viewer while the card tilts. Use as to render CardItem as any HTML element (heading, paragraph, link, or button).

Props

CardContainer

PropTypeDescription
childrenReact.ReactNodeThe card content
classNamestringClasses applied to the rotating inner wrapper
containerClassNamestringClasses applied to the perspective wrapper

CardBody

PropTypeDescription
childrenReact.ReactNodeThe card layers
classNamestringExtra classes (overrides the default h-96 w-96)

CardItem

PropTypeDescription
asReact.ElementTypeElement to render (defaults to div)
childrenReact.ReactNodeLayer content
classNamestringExtra classes
translateXnumber | stringX offset in px when hovered
translateYnumber | stringY offset in px when hovered
translateZnumber | stringDepth offset in px when hovered
rotateXnumber | stringExtra X rotation in deg when hovered
rotateYnumber | stringExtra Y rotation in deg when hovered
rotateZnumber | stringExtra Z rotation in deg when hovered

Features

  • Cursor Tracking: The card rotates toward the cursor relative to its center with a 1000px perspective
  • Parallax Layers: Every CardItem translates/rotates at its own depth, adding dimension to the surface
  • Context Driven: useMouseEnter exposes the hover state to let you animate children independently
  • Any Element: CardItem accepts an as prop so layers can be buttons, links, headings, images, or custom components
  • Themed: Uses the current design tokens (card surface, border, primary buttons, foreground/muted text)

Last updated on