Tilt 3D

A smooth 3D hover effect that brings interface elements to life with depth and motion.

Usage

tilt-3d-preview.tsx
import Tilt3D, { Tilt3DElement } from "@/registry/components/tilt-3d/tilt-3d";

const Tilt3DPreview = () => {
  return (
    <Tilt3D className="relative h-96 w-72 rounded-2xl">
      <img
        src="https://picsum.photos/id/296/320/480"
        alt="Mountain"
        className="absolute inset-0 h-full w-full object-cover rounded-[inherit]"
      />
      <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent rounded-[inherit]" />
      <Tilt3DElement z={30} className="absolute right-4 top-4">
        <span className="rounded-full bg-white/20 px-3 py-1 text-xs font-medium text-white">
          🏔️ Adventure
        </span>
      </Tilt3DElement>
      <Tilt3DElement z={25} className="absolute bottom-0 left-0 right-0 p-6 text-white">
        <p className="text-sm text-white/80">Switzerland</p>
        <h3 className="mt-1 text-3xl font-bold">
          Zermatt
        </h3>
        <p className="mt-2 text-sm leading-relaxed text-white/75">
          Discover breathtaking alpine peaks, scenic trails, and unforgettable mountain views.
        </p>
      </Tilt3DElement>
    </Tilt3D>
  );
};

export default Tilt3DPreview;

Component

Install the following packages before using this component.

Tailwind CSS
Motion
tilt-3d.tsx
import { createContext, memo, useCallback, useContext, useMemo, useRef, useState, type ComponentProps, type ComponentType, type MouseEventHandler } from "react";
import { motion, useMotionValue, useSpring, useTransform, type HTMLMotionProps, type HTMLElements } from "motion/react";
import cn from "@/utils/cn";

export type Tilt3DElementProps<T extends keyof HTMLElements> = {
  as?: keyof typeof motion<T>;
  z?: number;
} & HTMLMotionProps<T>; 

export type Tilt3DProps = {
  tiltX?: number;
  tiltY?: number;
  wrapperProps?: HTMLMotionProps<"div">,
} & ComponentProps<"div">;

const Tilt3DContext = createContext({
  isHovered: false,
});

export const Tilt3DElement = <T extends keyof HTMLElements,>(
  props: Tilt3DElementProps<T>,
) => {
  const {
    children,
    className,
    style,
    z = 0,
    as = "div",
    ...restProps
  } = props;

  const { isHovered } = useContext(Tilt3DContext);

  const Component = (motion[as]) as ComponentType<HTMLMotionProps<T>>;
  
  return (
    <Component
      {...restProps as HTMLMotionProps<T>}
      className={cn(
        "[transform-style:preserve-3d]",
        className,
      )}
      animate={{
        z: isHovered ? z: 0,
      }}
      style={{
        ...style,
        z: 0,
      }}
      transition={{
        type: "spring",
        stiffness: 200,
        damping: 50,
        mass: 0.5,
      }}
    >
      {children}
    </Component>
  )
};

const Tilt3D = (
  props: Tilt3DProps,
) => {
  const {
    children,
    className,
    tiltX = 25,
    tiltY = 25,
    wrapperProps = {},
    ...restProps
  } = props;

  const {
    className: wrapperClassName,
    style: wrapperStyle,
    ...restWrapperProps
  } = wrapperProps;

  const [isHovered, setIsHovered] = useState(false);

  const ref = useRef<HTMLDivElement>(null);

  const mouseX = useMotionValue(0);
  const mouseY = useMotionValue(0);

  const elementSize = useRef({
    width: 0,
    height: 0,
  });

  const springConfig = {
    stiffness: 200,
    damping: 50,
    mass: 0.5,
  };

  const rotateX = useSpring(
    useTransform(
      mouseY, 
      [0, elementSize.current.height / 2, elementSize.current.height],
      [tiltY, 0, -tiltY],
   ),
   springConfig,
  );

  const rotateY = useSpring(
    useTransform(
      mouseX, 
      [0, elementSize.current.width / 2, elementSize.current.width],
      [-tiltX, 0, tiltX],
   ),
   springConfig,
  );

  const handleMouseMove: MouseEventHandler<HTMLDivElement> = useCallback((event) => {
    if (!ref.current) return;
    const {top, left, width, height} = ref.current.getBoundingClientRect();
    mouseX.set(event.pageX - left);
    mouseY.set(event.pageY - top);
    elementSize.current.width = width;
    elementSize.current.height = height;
  }, [isHovered]);

  const handleMouseEnter = useCallback(() => {
    setIsHovered(true);
  }, []);

  const handleMouseLeave = useCallback(() => {
    setIsHovered(false);
  }, []);

  const tilt3DContextValue = useMemo(() => ({
    isHovered
  }), [isHovered]);

  if (!isHovered) {
    rotateX.set(0);
    rotateY.set(0);
  }
  
  return (
    <Tilt3DContext.Provider value={tilt3DContextValue}>
      <div
        {...restProps}
        className={cn(
          "[perspective:800px]",
          className,
        )}
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
        onMouseMove={handleMouseMove}
        ref={ref}
      >
        <motion.div
          {...restWrapperProps}
          className={cn(
            "w-full h-full rounded-[inherit] [transform-style:preserve-3d]",
            wrapperClassName,
          )} 
          style={{
            ...wrapperStyle,
            rotateX,
            rotateY,
          }}
        >
          {children}
        </motion.div>
      </div>
    </Tilt3DContext.Provider>
  );
};

export default memo(Tilt3D);

Component API

Tilt3D

PropTypeRequiredDefaultDescription
childrenReact.ReactNodeYesContent rendered inside the 3D tilt container.
tiltXnumberNo25Controls the horizontal (X-axis) tilt angle of the 3D effect.
tiltYnumberNo25Controls the vertical (Y-axis) tilt angle of the 3D effect.
wrapperPropsReact.HTMLAttributes<HTMLDivElement>NoProps forwarded to the wrapper element, including attributes like className, style, id, and event handlers.
classNamestringNoAdditional CSS classes applied to the wrapper element.

Tilt3DElement

PropTypeRequiredDefaultDescription
znumberNo0Z-axis translation value applied when the parent Tilt3D component is hovered. Higher values make the element appear elevated.
askeyof typeof motionNo"div"Motion element type to render. Supports any HTML element available through Framer Motion.
classNamestringNoAdditional CSS classes applied to the element.

Support the Project

If you find this component useful, consider starring the repository on GitHub. Found a bug or have a suggestion? Open an issue to help improve it.