KOMPOSE.
Toutes les ressources
Site WebClaude

Réussir des animations au scroll de très haute qualité

Le problème résolu

Le smooth scroll installé en trois lignes donne un site mou, qui tremble sur les sections épinglées et qui devient pénible sur mobile. Un scroll de niveau studio, ce sont quatre couches distinctes à régler séparément : inertie, révélation, épinglage, scrub. Ce guide donne les quatre, avec le code, les valeurs qui marchent et le démontage de trois sites qui les réussissent.

Le prompt à copier

Optimisé avecClaude
# Scroll motion layer: build spec
# Portable: save it as CLAUDE.md (Claude Code) or AGENTS.md (Codex), or paste
# it straight into a chat with Claude or ChatGPT.

You are implementing the scroll motion layer of a high-end marketing site.
Build it as ONE shared system, never as per-component animations.

Stack: GSAP + ScrollTrigger + Lenis. If the project is a React application,
use Framer Motion (useScroll / useTransform / useSpring) instead. Never both:
two animation loops always end up drifting apart.

1. INERTIA
   - Lenis with duration 1.1, smoothWheel: true, smoothTouch: false.
   - ONE clock only: lenis.on('scroll', ScrollTrigger.update), then
     gsap.ticker.add((t) => lenis.raf(t * 1000)) and gsap.ticker.lagSmoothing(0).
   - Never open a second requestAnimationFrame loop.
   - Add data-lenis-prevent to every internally scrollable panel: modals,
     menus, dropdowns, search panels, code blocks.
   - Remove any CSS scroll-behavior: smooth, it fights with Lenis on anchors.

2. REVEALS
   - One ScrollTrigger.batch pass over [data-anim="fade"] and [data-anim="line"].
   - start 'top 85%', once: true, opacity 0 to 1, y 12-24px, duration 0.6-0.9s,
     ease 'expo.out', stagger 0.04-0.08.
   - Headings and section intros only. Never every paragraph.
   - Call document.fonts.ready.then(() => ScrollTrigger.refresh()).

3. PINNING
   - position: sticky when an element only needs to stick.
   - ScrollTrigger pin + scrub: 0.5 when scroll progress drives an animation.
   - invalidateOnRefresh: true, and compute `end` as a function, not a constant.
   - Disable or drastically shorten every pin below 768px.

4. SCRUB
   - A paused , with currentTime = self.progress * video.duration,
     scrub 0.3.
   - Remind me to re-encode the source with `ffmpeg -g 1`, otherwise seeking
     stutters and the effect is unusable.

HARD CONSTRAINTS
   - Animate transform and opacity only. Never top, left, width, height, margin.
   - No getBoundingClientRect() inside a per-frame callback.
   - Wrap everything in gsap.matchMedia(). The (prefers-reduced-motion: reduce)
     branch must destroy Lenis and render the final state instantly. It must
     never leave elements sitting at opacity 0.
   - will-change only on the handful of elements actually being transformed.

Deliver: a single motion.ts file, the list of data attributes to add to the
markup, and an explicit list of what you deliberately chose not to animate.

Un bon scroll, c’est quatre couches, pas une librairie

L’erreur de départ, c’est de croire qu’on installe « le smooth scroll » et que le site devient premium. Un scroll de niveau studio, c’est quatre mécaniques indépendantes qu’on empile dans cet ordre :

  • L’inertie : le défilement lui-même est lissé, la page suit la molette avec une légère traîne.
  • La révélation : les éléments apparaissent quand ils entrent dans le champ.
  • L’épinglage : une section reste fixe pendant que son contenu progresse.
  • Le scrub : une animation, une vidéo ou une scène est pilotée image par image par la position de scroll.

Les sites qui impressionnent utilisent les quatre. Les sites qui donnent la nausée en utilisent trois au hasard, mal réglées, sur la même page.

Trois sites, trois écoles

Avant de coder, regarde ce que font ceux qui réussissent. Ces trois-là ont été inspectés directement, pas décrits de mémoire.

https://landonorris.com, https://jobs.netflix.com, https://www.rockstargames.com

landonorris.com : Webflow, plus une couche pilotée par attributs

Un site Webflow (attributs data-wf-site, assets sur le CDN Webflow) sur lequel une agence a greffé une vraie couche technique. Lenis est injecté via un simple embed contenant sa feuille de style (html.lenis, .lenis-smooth [data-lenis-prevent]). Et surtout, une vingtaine de <canvas> Rive pilotés par des attributs HTML : data-rive-scrolltrigger-target, data-rive-scrolltrigger-start, data-rive-scrolltrigger-end, data-rive-fit, data-rive-input.

Le pattern à voler est là : aucune animation n’est écrite dans le markup. Un seul script scanne le DOM, lit les attributs et branche ScrollTrigger. Un intégrateur peut ajouter une animation depuis Webflow sans toucher une ligne de JavaScript.

jobs.netflix.com : la sobriété qui tient à l’échelle

Next.js en App Router, avec un composant LenisProvider qui enveloppe l’application. Pas de 3D, pas d’effet spectaculaire : un site de recrutement qui doit rester utilisable. Le détail qui trahit le travail sérieux : le panneau de recherche porte data-lenis-prevent="true". Ce panneau a son propre scroll interne, donc il est exclu du lissage global. Sans cet attribut, la molette dans le panneau ferait bouger la page derrière. C’est exactement le genre de détail qui sépare une intégration propre d’un copier-coller de documentation.

rockstargames.com : le scroll déclaratif en React

Le HTML servi fait 3,5 Ko. Tout le site est assemblé en micro-frontends chargés dynamiquement (quinze modules indépendants, versionnés séparément, servis depuis un CDN). Dans les dépendances partagées déclarées : React, React Router et framer-motion 12.42.2. Pas de GSAP, pas de Lenis. Leurs animations de scroll sont écrites en React, dans les composants, avec useScroll et useTransform. C’est une approche complètement différente des deux autres, et parfaitement valide : quand le site est une application React à plusieurs équipes, une animation déclarative dans le composant se maintient mieux qu’un script global qui scanne le DOM.

Retiens la règle : site vitrine ou Webflow, prends GSAP plus Lenis. Application React, prends Framer Motion. Mélanger les deux sur un même projet, c’est deux boucles d’animation concurrentes et des saccades garanties.

Couche 1 : l’inertie

C’est la couche la plus visible et la plus vite ratée. Le principe : le navigateur ne défile plus, on intercepte l’événement et on interpole la position à chaque image.

import Lenis from 'lenis'
import gsap from 'gsap'
import ScrollTrigger from 'gsap/ScrollTrigger'

gsap.registerPlugin(ScrollTrigger)

const lenis = new Lenis({
  duration: 1.1,          // 0.8 a 1.2. Au-dela, le site parait mou.
  smoothWheel: true,
  smoothTouch: false,     // on laisse le scroll natif au doigt
})

// Une seule horloge pour tout le site : celle de GSAP.
lenis.on('scroll', ScrollTrigger.update)
gsap.ticker.add((time) => lenis.raf(time * 1000))
gsap.ticker.lagSmoothing(0)

Ces cinq lignes de branchement sont le cœur du sujet. Si tu fais tourner Lenis dans sa propre boucle requestAnimationFrame et GSAP dans la sienne, les deux se décalent d’une image et tout ce qui est épinglé se met à trembler. Une seule horloge, toujours.

Les quatre erreurs qui reviennent :

  • Une durée trop longue. Au-delà de 1,2, le site ne paraît pas fluide, il paraît lent à répondre. La sensation de qualité vient de la réactivité, pas de la traîne.
  • Cumuler avec scroll-behavior: smooth en CSS. Les deux se battent, les ancres deviennent imprévisibles. Retire le CSS.
  • Oublier les panneaux à scroll interne. Modales, menus, listes déroulantes : chacun reçoit data-lenis-prevent, comme sur le site de recrutement Netflix.
  • Forcer l’inertie sur mobile. Le scroll tactile natif est déjà parfaitement lié au doigt. Le lisser crée un décalage perçu immédiatement comme un bug.

Couche 2 : les révélations

Un seul fichier, aucune animation dans les composants. On déclare l’intention en HTML, le script s’occupe du reste :

// Une passe, tous les elements marques du site
ScrollTrigger.batch('[data-anim="fade"]', {
  start: 'top 85%',
  once: true,
  onEnter: (els) => gsap.to(els, {
    opacity: 1,
    y: 0,
    duration: 0.8,
    ease: 'expo.out',
    stagger: 0.06,
  }),
})

Les valeurs qui marchent, après beaucoup d’essais : un déplacement de 12 à 24 pixels (au-delà, ça saute aux yeux), une durée de 0,6 à 0,9 seconde, un ease en expo.out ou power3.out, un décalage de 0,04 à 0,08 seconde entre les éléments d’un groupe, et un déclenchement à top 85% pour que l’animation soit déjà commencée quand l’œil arrive dessus.

Le once: true n’est pas un détail. Une animation qui rejoue à chaque remontée transforme la lecture en attraction foraine. On révèle une fois, et on n’anime que les titres et les intros de section, jamais chaque paragraphe.

Dernier point : recalcule les positions quand les polices sont chargées, sinon tous tes déclencheurs sont calés sur une mise en page qui n’existe plus.

document.fonts.ready.then(() => ScrollTrigger.refresh())

Couche 3 : l’épinglage

Une section qui reste fixe pendant que son contenu avance. C’est ce qui donne l’impression que le site est un film plutôt qu’une page. Deux voies : position: sticky en CSS, gratuit et increvable, ou le pin de ScrollTrigger, plus puissant mais qui modifie le DOM.

La règle simple : si tu veux juste qu’un élément colle, utilise sticky. Si tu dois lier la progression du scroll à une animation, utilise pin avec scrub. Le cas classique, la section horizontale :

const panels = gsap.utils.toArray('.panel')

gsap.to(panels, {
  xPercent: -100 * (panels.length - 1),
  ease: 'none',
  scrollTrigger: {
    trigger: '.horizontal',
    pin: true,
    scrub: 0.5,               // la latence : c'est elle qui lisse
    end: () => '+=' + window.innerWidth * panels.length,
    invalidateOnRefresh: true,
  },
})

scrub: 0.5 plutôt que scrub: true : l’animation rattrape la position de scroll avec un demi-seconde de retard, ce qui absorbe les à-coups de la molette. C’est un réglage à une valeur qui change tout le ressenti.

Sur mobile, désactive l’épinglage ou réduis-le drastiquement. Une section horizontale épinglée sur 400 % de hauteur, sur un téléphone, donne surtout l’impression que la page est bloquée.

Couche 4 : le scrub

La couche qui impressionne vraiment : une vidéo ou une animation dont chaque image correspond à une position de scroll. Trois techniques, par ordre de rentabilité.

Le scrub vidéo

Une balise <video> en pause dont on pilote le currentTime. Beaucoup plus léger qu’une séquence de 300 PNG, et c’est ce qu’utilisent les grandes pages produit aujourd’hui.

const video = document.querySelector('[data-scrub-video]')
video.pause()

ScrollTrigger.create({
  trigger: '.sequence',
  start: 'top top',
  end: '+=200%',
  pin: true,
  scrub: 0.3,
  onUpdate: (self) => {
    if (video.duration) video.currentTime = self.progress * video.duration
  },
})

Le piège, c’est l’encodage. Une vidéo normale n’a qu’une image-clé toutes les deux secondes : le déplacement dans le temps saccade parce que le décodeur doit reconstruire chaque image. Il faut réencoder avec une image-clé partout :

ffmpeg -i source.mp4 -an -vf "scale=1280:-2" -c:v libx264 -crf 26 -g 1 
  -movflags +faststart scrub.mp4

Le -g 1 force chaque image en image-clé. Le fichier grossit d’environ 40 %, mais le scrub devient parfaitement fluide. Sans lui, l’effet est inutilisable.

Rive

Pour du motion vectoriel, c’est supérieur à tout le reste : quelques dizaines de kilo-octets, net à toutes les résolutions, et une machine d’états qu’on pilote depuis le code. C’est le choix fait sur le site de Lando Norris, avec la progression du scroll envoyée directement dans les entrées de l’animation. Un designer travaille dans l’éditeur Rive, le développeur ne fait que brancher.

La scène 3D

Three.js piloté au scroll, c’est le niveau au-dessus, et un autre budget. À ne considérer que si l’objet 3D est le produit lui-même.

La variante React : le scroll déclaratif

Si le projet est une application React, pas besoin de scanner le DOM. La progression du scroll devient une valeur réactive :

const ref = useRef(null)
const { scrollYProgress } = useScroll({
  target: ref,
  offset: ['start end', 'end start'],
})
const y = useTransform(scrollYProgress, [0, 1], ['0%', '-25%'])
const smooth = useSpring(y, { stiffness: 120, damping: 20, mass: 0.4 })

return <motion.div ref={ref} style={{ y: smooth }} />

Le useSpring autour du useTransform joue le rôle du scrub de GSAP : il amortit le mouvement au lieu de le coller image par image à la molette. Sans lui, le résultat paraît nerveux et bon marché.

Les règles de performance

Une animation au scroll dispose de 16 millisecondes par image. Ce qui la fait sortir du budget :

  • N’anime que transform et opacity. Animer top, left, width, height ou margin force un recalcul de mise en page à chaque image. C’est la cause numéro un des saccades.
  • will-change: transform avec parcimonie, et jamais sur des dizaines d’éléments : chaque couche promue consomme de la mémoire vidéo.
  • Pas de lecture du DOM dans une boucle. Un getBoundingClientRect() appelé à chaque image annule tout le bénéfice. ScrollTrigger met les positions en cache, laisse-le faire.
  • Un seul observateur global plutôt qu’un par composant.
  • Vérifie sur un vrai téléphone milieu de gamme, pas sur ton portable. C’est là que se jouent les avis.

L’accessibilité, la partie qu’on saute toujours

Pour une partie des visiteurs, le mouvement provoque de vrais troubles. Le système le signale, il suffit d’écouter :

const mm = gsap.matchMedia()

mm.add('(prefers-reduced-motion: no-preference)', () => {
  // toutes les animations vivent ici
})

mm.add('(prefers-reduced-motion: reduce)', () => {
  lenis.destroy()
  gsap.set('[data-anim]', { opacity: 1, y: 0, clearProps: 'transform' })
})

Le réflexe important : la version réduite affiche l’état final immédiatement. Elle ne désactive pas l’animation en laissant les éléments invisibles, ce qui est l’un des bugs les plus fréquents et les plus graves : un site entièrement vide pour l’utilisateur. Vérifie aussi que la navigation au clavier fonctionne dans les sections épinglées, et que les ancres tombent au bon endroit après un rechargement.

Ce que le CSS sait déjà faire tout seul

Pour les révélations simples, le JavaScript n’est déjà plus obligatoire :

@keyframes reveal {
  from { opacity: 0; translate: 0 24px; }
  to   { opacity: 1; translate: 0 0; }
}

.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 10% cover 35%;
}

Animé par le compositeur, sans une ligne de JS, sans coût de boucle. Le support est encore partiel : on l’utilise en enrichissement, avec un @supports et un état final visible par défaut. Pour un site où le scroll n’est pas le sujet principal, c’est souvent tout ce dont tu as besoin, et ça évite de charger 50 Ko de librairie pour trois fondus.

Brancher le prompt sur Claude Code ou Codex

Le prompt en haut de cette fiche n’est pas fait pour être collé une fois puis oublié. C’est un fichier de contraintes : il vit à la racine du projet et l’agent le relit à chaque session. D’un agent à l’autre, seul le nom du fichier change.

  • Claude Code : CLAUDE.md à la racine du dépôt.
  • Codex : AGENTS.md, même contenu au caractère près. C’est la convention qu’il va chercher.
  • Les deux sur le même projet : garde AGENTS.md comme source unique et réduis CLAUDE.md à une ligne d’import, @AGENTS.md. Une seule version à maintenir, pas deux qui divergent au bout d’une semaine.
  • En conversation simple, sans agent (Claude ou ChatGPT dans le navigateur) : colle le prompt, puis envoie le fichier de la section à traiter. Sans le contexte du dépôt, demande le code d’un seul bloc à la fois.

Deux réflexes valables quel que soit l’agent. Le premier : une couche à la fois, avec validation entre chaque, jamais les quatre d’un coup. Le second : fais-lui décrire en trois puces ce qu’il s’apprête à écrire avant qu’il écrive la moindre ligne. Corriger une description coûte dix secondes, corriger trois cents lignes en coûte trente minutes.

Une limite à connaître : sans outil de capture branché, aucun des deux ne voit son propre rendu. Les valeurs chiffrées de ce guide (déclenchement à 85 %, 0,8 seconde, expo.out, scrub: 0.5) servent exactement à ça : elles remplacent l’œil tant que tu n’as pas mis une boucle visuelle en place.

La checklist avant de livrer

  • Une seule boucle d’animation, celle de GSAP, avec Lenis branché dessus.
  • data-lenis-prevent sur chaque zone à scroll interne.
  • ScrollTrigger.refresh() après le chargement des polices et des images.
  • Révélations en once: true, réservées aux titres et intros.
  • Épinglages désactivés ou raccourcis sous 768 pixels.
  • Vidéos de scrub réencodées en -g 1.
  • Branche prefers-reduced-motion testée pour de vrai, en activant l’option système.
  • Test sur téléphone milieu de gamme, pas seulement au simulateur.
  • Ancres, retour arrière et restauration de position vérifiés.

Le scroll est la seule interaction que 100 % de tes visiteurs vont utiliser. C’est aussi la seule qu’ils remarquent quand elle est ratée. Trois jours de réglage dessus se voient plus qu’un mois passé sur une fonctionnalité que personne n’ouvre.

Ressources liées

Toutes les ressources
Site WebClaude

Reproduire une page produit Apple de A à Z (le démontage du code)

Les pages produit d'Apple passent pour hors de portée, alors qu'elles reposent sur cinq couches empilables et toutes reproductibles. Cette ressource ouvre les bundles JavaScript livrés en production sur iPhone 17 Pro, AirPods Pro et MacBook Pro, montre ce que chacune fait vraiment (shaders GLSL maison, scrub vidéo en HLS, moteur de keyframes piloté en CSS), puis donne l'ordre de construction et le prompt complet.

›_You are building an Apple-style product page: a long scroll where the visuals are driven b
Voir la fiche
Site WebClaude

Produire un site à 10 000 € avec l'IA : le pipeline complet

Un client montre igloo.inc et demande un devis. Ces sites paraissent inaccessibles en solo, alors que deux des trois reposent sur une stack très reproductible. Ce guide donne le démontage réel des trois références, ce que 10 000 € achètent vraiment, et le pipeline complet de production avec Claude et les outils externes qui vont avec.

›_# CLAUDE.md - Project constitution for a studio-grade marketing site You are the lead crea
Voir la fiche
Site WebClaude

Comment un site comme likova.space est construit (et le prompt pour le recreer)

Les sites immobilier/architecture comme likova.space paraissent etre une grosse production mais reposent sur une stack simple : pas de framework, Three.js brut avec GLTFLoader pour un vrai modele 3D, mouvement au scroll fait a la main. Ce guide detaille exactement ce qui tourne et donne un prompt pret a l'emploi pour recreer le pattern avec son propre projet.

›_You are building a scroll-narrative marketing site for a physical product (real estate, ar
Voir la fiche
Passer à l'action

On vous construit le site ?

Ces ressources donnent le ton. On peut livrer le site complet, rapide et pensé pour convertir.

Réserver un appelÀ propos de nous