3D Card Effect A card perspective effect — hover over the card to elevate card elements.
A card perspective effect: hovering over the card rotates it in 3D space while each CardItem layer lifts independently, elevating elements toward the viewer.
npm pnpm yarn bun
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 "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 context export 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; Expand
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).
Prop Type Description childrenReact.ReactNodeThe card content classNamestringClasses applied to the rotating inner wrapper containerClassNamestringClasses applied to the perspective wrapper
Prop Type Description childrenReact.ReactNodeThe card layers classNamestringExtra classes (overrides the default h-96 w-96)
Prop Type Description 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
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)