Retour au catalogue

Values Parallax Cards

Cartes de valeurs avec effet de profondeur parallaxe au mouvement de la souris.

valuesmedium Both Responsive a11y
elegantboldagencyportfoliouniversalgrid
Theme

How to build 3D parallax tilt cards in React

A 3D tilt card in React tracks the mouse position relative to each card, maps it to rotateX/rotateY values via Framer Motion's useTransform, then smooths the rotation with useSpring so the tilt follows the cursor with natural inertia and springs back to flat on mouse leave.

  • Stack: React + Framer Motion + Tailwind v4 + Lucide React, ~90 lines total, no extra dependencies.
  • Core API: useMotionValue, useTransform, useSpring, one set of motion values per card instance.
  • perspective: 800 on the outer div, transformStyle: preserve-3d on the card, translateZ: 40 on the content layer for depth.
  • Accessible: semantic h3 headings and Lucide icons rendered as decorative (no aria-label needed for icons already paired with text).
  • Touch devices see the stagger entrance animation but no tilt, mouse events don't fire on tap.

Values Parallax Cards is a responsive grid of company-value cards where each card tilts in 3D toward the cursor as you hover over it. The content layer floats forward with a translateZ offset, reinforcing the depth illusion. Cards enter sequentially with a staggered fade-up, then respond independently to mouse movement, each one tracks its own pointer coordinates.

Anatomy

The section wraps a centered header (optional badge, h2, subtitle) and a 1-2-3-column responsive grid. Each grid cell is a ParallaxCard, an outer motion.div that holds the perspective context and listens for mouse events, an inner motion.div that receives rotateX/rotateY and carries the border and background, and a third nested motion.div with translateZ: 40 that pushes the icon, title, and paragraph toward the viewer.

How it works

On each mouse move, the handler reads the card's bounding rect and computes normalized x/y values in the range [-0.5, 0.5]. These feed into useMotionValue (mouseX and mouseY). useTransform maps the normalized values to rotation angles: mouseY drives rotateX between 8 and -8 degrees, mouseX drives rotateY between -8 and 8. Each rotation is wrapped in useSpring with stiffness 200 and damping 20, so the card follows the cursor with a subtle elastic lag. On mouse leave, both motion values are reset to 0 and the springs return the card to flat.

How to build it in React

  1. Set up the perspective container

    Wrap each card in a motion.div with style={{ perspective: 800, transformStyle: 'preserve-3d' }}. This div is the one that captures mouse events, it does not rotate itself, it only provides the 3D projection context for the child.

    const ref = useRef<HTMLDivElement>(null);
    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    
    // Outer wrapper, holds perspective, captures mouse
    <motion.div ref={ref} style={{ perspective: 800, transformStyle: "preserve-3d" }}
      onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave}>
  2. Derive rotation springs from mouse position

    Normalize the mouse position to [-0.5, 0.5] relative to the card, then pipe it through useTransform to get rotation angles. Wrap both in useSpring to add inertia. Rotation ranges of ±8 degrees look subtle and readable; go higher only for large cards where tilt stays comfortable.

    const rotateX = useSpring(
      useTransform(mouseY, [-0.5, 0.5], [8, -8]),
      { stiffness: 200, damping: 20 }
    );
    const rotateY = useSpring(
      useTransform(mouseX, [-0.5, 0.5], [-8, 8]),
      { stiffness: 200, damping: 20 }
    );
  3. Apply rotateX/rotateY to the card surface

    The inner motion.div receives the rotation springs as its style. Keep transformStyle: 'preserve-3d' here too so the content layer can translateZ out of the card plane. Add a hover:shadow-2xl class for the depth shadow that completes the lift effect.

    <motion.div style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
      className="rounded-xl border p-8 hover:shadow-2xl">
      <motion.div style={{ translateZ: 40 }}>
        {/* icon, title, description */}
      </motion.div>
    </motion.div>
  4. Stagger the entrance animation

    Pass the card index to the outer motion.div's transition delay: `delay: index * 0.1`. Pair it with whileInView so the stagger fires as the grid scrolls into view, not on page load. Set viewport={{ once: true }} to avoid replaying on scroll back.

    <motion.div
      initial={{ opacity: 0, y: 24 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ delay: index * 0.1, duration: 0.5 }}
    >

When to use it

Reach for this pattern on brand, agency, or portfolio pages where you want values or features to feel premium. The 3D tilt signals craftsmanship without heavy visuals. Skip it on dashboards, data-heavy pages, or anywhere cards need a click interaction, the hover tilt competes with click affordance. On mobile the tilt is absent, so make sure the cards read well flat. Limit the grid to 3-6 items; more than six cards makes the stagger entrance feel slow.

Used by

  • Stripe, Uses subtle 3D card tilt on product feature sections to add depth without distracting from content.
  • Linear, Feature cards on the marketing site respond to pointer movement with perspective transforms, reinforcing the premium feel.
  • Lottiefiles, Category and feature cards use mouse-tracking tilt to make the animation-first brand feel interactive throughout.

FAQ

Why use a ref to compute mouse position instead of a global event listener?

getBoundingClientRect on the card ref gives you coordinates relative to that specific card, so the tilt is always centered on the card the cursor is over. A global listener would require subtracting the card's page offset manually on every event.

Why wrap useTransform in useSpring rather than applying spring to the raw mouse values?

The mouse values themselves should update instantly, adding a spring there would make the normalized position lag, which looks wrong. Applying spring after the transform means the rotation angle smooths out while the coordinate tracking stays sharp.

How do I make the tilt more or less pronounced?

Change the output range in useTransform: [8, -8] for rotateX and [-8, 8] for rotateY. Values between 5 and 12 degrees work well for typical card sizes. Going past 15 degrees tends to distort text readability. The translateZ value on the content layer (currently 40) also affects perceived depth, lower it for a flatter look.

Does the tilt work on touch screens?

No. onMouseMove does not fire on touch events. Cards still get the stagger entrance animation and look correct at rest. If you want a touch version, listen to deviceorientation and map device tilt angles to the same rotateX/rotateY motion values.

"use client";

import React, { useRef } from "react";
import { motion, useMotionValue, useTransform, useSpring } from "framer-motion";
import * as LucideIcons from "lucide-react";

interface ValueItem {
  id: string;
  title: string;
  description: string;
  icon?: string;
}

interface ValuesParallaxCardsProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  values: ValueItem[];
}

function getIcon(name?: string) {
  if (!name) return null;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React 3D Tilt Cards for Values Section, Framer Motion