Retour au catalogue

Hero Perspective Zoom

Hero cinématique avec entrée 3D : le contenu démarre incliné (rotateX 15°, scale 0.9) et se redresse progressivement au scroll. Inclut un mockup dashboard avec son propre effet de tilt.

herocomplex Both Responsive a11y
boldelegantcorporatesaasagencyportfoliocentered
Theme

Créer un hero React avec tilt 3D piloté au scroll

Un hero React à perspective pilotée au scroll applique un conteneur CSS perspective et utilise useScroll et useTransform de Framer Motion pour convertir la progression du scroll en valeurs rotateX et scale, lissées par useSpring pour que l'inclinaison se résorbe naturellement quand l'utilisateur défile.

  • Stack : React + Framer Motion + Lucide React, ~480 lignes, zéro dépendance runtime supplémentaire.
  • API Framer Motion utilisée : useScroll, useTransform, useSpring, transformStyle: preserve-3d.
  • Le mockup dashboard tourne sur son propre axe de tilt (rotateX 22° à 2°), légèrement plus marqué que la couche texte.
  • Accessible : HTML sémantique, aria-hidden sur les divs décoratifs, contenu lisible à toutes les positions de scroll.
  • L'effet 3D est pleinement visible sur desktop ; sur mobile les mêmes transformations s'appliquent mais la compression perspective est moins marquée sur petits écrans.

Ce hero s'ouvre incliné vers le visiteur, le bloc texte à 15° sur l'axe X et le mockup dashboard à 22°, puis se redresse en un layout plat au fil du scroll. Le résultat ressemble à un recul de caméra cinématique, le type d'entrée qui signale un produit premium sans recourir à une vidéo de fond.

Anatomie

La section contient deux couches décoratives (une fine ligne d'accent en haut et un halo flou sous le mockup), puis un seul wrapper perspective à 1200px. À l'intérieur, une motion.div porte le bloc texte et une motion.div imbriquée pour le mockup, chacune avec ses propres springs rotateX et scale. Le bloc texte se centre à 700px max-width ; le mockup se place en dessous à 900px max-width avec un ratio 16:9, affichant une fausse barre de chrome et un dashboard deux colonnes (nav latérale + cartes de stats + graphique barres).

Comment ça marche

Le hook scroll s'attache à l'élément section via useRef et lit scrollYProgress entre les offsets ["start start", "end start"]. Deux transformations brutes convertissent cette progression : rotateX passe de 15° à 0° sur la première moitié du scroll, et scale de 0.9 à 1.0 sur la même plage. L'opacité monte de 0.7 à 1.0 sur le premier quart. Chaque valeur brute traverse un useSpring (stiffness 60, damping 20) pour que l'animation traîne légèrement et semble physique. Le mockup utilise des springs séparés avec un angle de départ plus grand (22° à 2°) et une scale plus petite (0.88), créant une profondeur de type parallaxe entre les deux couches malgré une source de scroll commune.

Comment le coder en React

  1. Brancher la source de scroll et les transformations brutes

    Attache un ref à la section, puis appelle useScroll avec cette cible et la paire d'offsets. Passe scrollYProgress dans des appels useTransform pour dériver les plages rotateX, scale et opacity. Garde la plage d'entrée étroite (0 à 0.5) pour que l'effet se résorbe avant que la section quitte le viewport.

    const sectionRef = useRef<HTMLElement>(null);
    const { scrollYProgress } = useScroll({
      target: sectionRef,
      offset: ["start start", "end start"],
    });
    const rawRotateX = useTransform(scrollYProgress, [0, 0.5], [15, 0]);
    const rawScale   = useTransform(scrollYProgress, [0, 0.5], [0.9, 1]);
    const rawOpacity = useTransform(scrollYProgress, [0, 0.25], [0.7, 1]);
  2. Lisser les valeurs brutes avec des springs

    Enveloppe chaque motion value brute dans useSpring avec une config cohérente. Une stiffness faible (60) et un damping modéré (20) produisent l'effet de traîne sans oscillation. C'est le spring qui distingue un tilt cinématique d'une transition CSS rigide.

    const SPRING = { stiffness: 60, damping: 20, mass: 1 };
    const rotateX = useSpring(rawRotateX, SPRING);
    const scale   = useSpring(rawScale,   SPRING);
    const opacity = useSpring(rawOpacity, SPRING);
  3. Appliquer le conteneur perspective et la couche motion

    Enveloppe le tout dans un div ordinaire avec `perspective: 1200px` et `perspectiveOrigin: "50% 30%"`. La motion.div intérieure reçoit les valeurs spring et doit avoir `transformStyle: "preserve-3d"` et `transformOrigin: "50% 0%"` pour que le tilt pivote depuis le bord supérieur, pas depuis le centre.

    <div style={{ perspective: "1200px", perspectiveOrigin: "50% 30%" }}>
      <motion.div
        style={{ rotateX, scale, opacity,
                 transformStyle: "preserve-3d",
                 transformOrigin: "50% 0%" }}
      >
        {/* text + mockup */}
      </motion.div>
    </div>
  4. Donner au mockup son propre tilt plus prononcé

    Imbrique une deuxième motion.div pour le mockup avec sa propre paire useTransform + useSpring. Démarre-la à 22° au lieu de 15° et ne la ramène qu'à 2° (pas à 0°) pour conserver une légère profondeur une fois le hero entièrement affiché. Cet écart entre les deux couches se lit comme du parallaxe.

    const rawMockupRotateX = useTransform(scrollYProgress, [0, 0.5], [22, 2]);
    const rawMockupScale   = useTransform(scrollYProgress, [0, 0.5], [0.88, 1]);
    const mockupRotateX = useSpring(rawMockupRotateX, SPRING);
    const mockupScale   = useSpring(rawMockupScale,   SPRING);

Quand l'utiliser

Ce pattern fonctionne mieux sur une landing SaaS ou produit où tu as une vraie capture d'écran ou un mockup à montrer. L'inclinaison attire l'attention sur l'image produit et cadre la page comme une révélation. À éviter si la section above-the-fold porte déjà des animations lourdes, ou si l'action de conversion principale doit être immédiatement visible sans distraction visuelle. Comme l'effet est déclenché au scroll, il perd aussi son sens sur les pages courtes où le hero occupe l'essentiel de la hauteur du viewport.

Utilisé par

  • Linear, Utilise une perspective 3D pilotée au scroll sur son hero produit pour faire apparaître le mockup depuis un angle incliné.
  • Vercel, Applique des transformations de scale et de profondeur aux captures produit dans les heros, donnant l'impression que l'interface émerge vers le visiteur.
  • Loom, Utilise une entrée perspective cinématique pour son mockup d'app sur la homepage marketing, passant d'un angle 3D compressé à une vue plate.

FAQ

Pourquoi useSpring plutôt qu'appliquer useTransform directement à la motion.div ?

useTransform mappe la progression du scroll de façon linéaire, donc le tilt suit exactement la vitesse de scroll et semble mécanique. useSpring ajoute de la masse et du damping pour que le tilt accuse un léger retard sur la position de scroll, puis rattrape, ce retard donne une impression de poids physique plutôt que d'animation CSS.

L'effet se casse-t-il si l'utilisateur a activé la préférence reduced-motion ?

Le composant ne vérifie pas prefers-reduced-motion pour l'instant. Pour le respecter, lis la media query avec useReducedMotion de Framer Motion et initialise rotateX et scale à leurs valeurs finales (0° et 1.0) pour supprimer toute transformation.

Peut-on remplacer le mockup intégré par une vraie capture d'écran ?

Oui. Le mockup est dans sa propre motion.div avec un ratio 16:9 fixé. Remplace le contenu intérieur par une <img> ou une <Image> Next.js, le spring perspective qui l'enveloppe reste identique. Garde overflow:hidden et border-radius sur le conteneur extérieur pour que l'image se découpe correctement.

Comment rendre le tilt plus lourd ou plus vif ?

Modifie la config SPRING : une stiffness plus faible (ex. 30) et une masse plus élevée le rendent lent et cinématique ; une stiffness plus haute (ex. 120) avec moins de damping le fait claquer. Les valeurs de départ rotateX (15° et 22°) contrôlent le dramatisme de l'angle initial, monte-les à 25°/35° pour une entrée plus exagérée.

"use client";

import { useRef } from "react";
import { motion, useScroll, useTransform, useSpring } from "framer-motion";
import { ArrowRight } from "lucide-react";

interface HeroPerspectiveZoomProps {
  badge?: string;
  title?: string;
  titleAccent?: string;
  description?: string;
  ctaLabel?: string;
  ctaUrl?: string;
  ctaSecondaryLabel?: string;
  ctaSecondaryUrl?: string;
  mockupLabel?: string;
}

const EASE = [0.16, 1, 0.3, 1] as const;
const SPRING = { stiffness: 60, damping: 20, mass: 1 };

export default function HeroPerspectiveZoom({

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Avis

Hero React 3D au scroll (tilt perspective), Code + Tutoriel