🧑🏼‍💻
Miika Huttunen

3D Card Effect

Google Gemini Effect3D Card EffectSparklesBackground GradientSpotlightImages SliderHero ParallaxWavy BackgroundGlowing StarsText Reveal CardEvervault CardAnimated TooltipFollowing PointerBackground BeamsSVG Mask EffectInfinite Moving CardsGrid and Dot Backgrounds3D PinParallax ScrollTracing BeamContainer Scroll AnimationText Generate EffectMeteorsCard StackMoving BorderLamp effectCard Hover EffectSticky Scroll RevealLayout GridDirection Aware HoverFloating NavbarNavbar MenuTabsTailwind CSS buttonsGradient AnimationBento GridTypewriter EffectBackground Boxes

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

Make things float in air

Hover over this card to unleash the power of CSS perspective

image

Installation

Install dependencies

npm i framer-motion clsx tailwind-merge

Add util file

utils/cn.ts

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

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

Copy the source code

components/ui/3d-card.tsx

Examples

With rotation

Make things float in air

Hover over this card to unleash the power of CSS perspective

Props

CardContainer

Prop
Type
Description
children
React.ReactNode
The child elements to be rendered within the container.
className
string
The CSS class to be applied to the container.
containerClassName
string
The CSS class to be applied to the outer container.

CardBody

Prop
Type
Description
children
React.ReactNode
The child elements to be rendered within the body.
className
string
The CSS class to be applied to the body.

CardItem

Prop
Type
Description
as
React.ElementType
The HTML tag to be used for the item. Defaults to "div".
children
React.ReactNode
The child elements to be rendered within the item.
className
string
The CSS class to be applied to the item.
translateX
number or string
The X translation of the item.
translateY
number or string
The Y translation of the item.
translateZ
number or string
The Z translation of the item.
rotateX
number or string
The X rotation of the item.
rotateY
number or string
The Y rotation of the item.
miikahuttunen
"use client";

import { cn } from "@/utils/cn";
import Image from "next/image";
import React, {
createContext,
useState,
useContext,
useRef,
useEffect,
} 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 = (e: React.MouseEvent<HTMLDivElement>) => {
setIsMouseEntered(true);
if (!containerRef.current) return;
};

const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>) => {
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>
);
};

export const CardItem = ({
as: Tag = "div",
children,
className,
translateX = 0,
translateY = 0,
translateZ = 0,
rotateX = 0,
rotateY = 0,
rotateZ = 0,
...rest
}: {
as?: React.ElementType;
children: React.ReactNode;
className?: string;
translateX?: number | string;
translateY?: number | string;
translateZ?: number | string;
rotateX?: number | string;
rotateY?: number | string;
rotateZ?: number | string;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [isMouseEntered] = useMouseEnter();

useEffect(() => {
handleAnimations();
}, [isMouseEntered]);

const handleAnimations = () => {
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)`;
}
};

return (
<Tag
ref={ref}
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;
};