Scroll✨ Premium

Pinned Image Sequence

Un vrai modèle glTF — ici une paire de lunettes de démonstration, 371 Ko, 13 396 triangles, verres iridescents — rendu par le composant model-viewer (chargé depuis un CDN) et épinglé en position: sticky dans une piste de 320 % : chaque événement scroll réécrit camera-orbit, 17° par cran de molette, 21 crans pour le tour, et la caméra rattrape sa cible en 230 ms. Le modèle est à remplacer par le vôtre.

ScrollSticky3Dmodel-viewer

Vous aurez accès à la démo interactive avec un compte gratuit.

Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Scroll comprend 70 effets, dont 12 gratuits. Découvrir la catégorie →

3 exemples d'usage

Fiche produit sneaker — e-commerce premium — Pinned Image Sequence exemple 1

① Fiche produit sneaker — e-commerce premium

QuandFiche d'une chaussure haut de gamme : colonne d'achat (pointures, prix, panier) à gauche, la visionneuse en média principal à droite.
PourquoiLe client inspecte une géométrie réelle — semelle, coutures, logo — que le défilement fait pivoter sur 360° ; Image Sequence (fx-0583) commute 24 états CSS en dégradés et ne peut pas montrer une semelle. Le sneaker de cette scène (embarqué dans le fichier de scènes, à la place des lunettes de démonstration du code) a en plus trois coloris KHR_materials_variants (Midnight, Beach, Street) qu'un attribut variant-name suffit à activer.
RéglagesModèle sneaker propre à la scène (903 Ko, en data-URI), zone de 304 × 360 px (aspect-ratio neutralisé, hauteur fixée), viewer 240 px, indication déplacée en bas, course de 792 px ; loading="eager" pour charger avant l'affichage.
Catalogue de pièces détachées — contrôle du marquage — Pinned Image Sequence exemple 2

② Catalogue de pièces détachées — contrôle du marquage

QuandFiche d'un raccord à bride dans un catalogue de maintenance : référence, matière, pression, stock, bouton de commande.
PourquoiIdentifier une pièce demande sa géométrie exacte — filetage, bride, écrou — et le marquage gravé se lit en la faisant pivoter ; aucune simulation CSS ne reproduit un pas de vis. L'élévation figée à 75° montre le dessus et les flancs : pour lire un dessous, changez le 75deg de la chaîne dans update().
RéglagesModèle glTF généré (raccord, 240 Ko, matériaux métalliques PBR : acier zingué, laiton, joint bruni), zone de 250 × 316 px sur fond gris industriel, course de 695 px, indication forcée sur une ligne (white-space: nowrap, elle se replie sous 324 px), fiche technique en <dl> à droite.
Artefact archéologique en 3D — page d'un musée — Pinned Image Sequence exemple 3

③ Artefact archéologique en 3D — page d'un musée

QuandPage d'une amphore de la collection : cartel en surimpression, réservation d'une visite et lien vers la version en réalité augmentée.
PourquoiLe visiteur fait le tour de l'objet comme devant la vitrine, et le même fichier glTF sert l'AR : model-viewer l'exporte avec l'attribut ar (Scene Viewer sur Android, Quick Look sur iOS) — un pont qu'aucune séquence d'images ne permet.
RéglagesModèle glTF généré (amphore, 300 Ko, terre cuite mate et vernis noir), zone plein cadre 540 × 360 px, viewer agrandi à 250 px, compteur d'angle déplacé en haut à droite pour libérer le cartel, course de 792 px.

Comment ça marche

La zone .pis-scene (width: 100%, aspect-ratio: 4/3, overflow: hidden, isolation: isolate, dégradé radial #1b2350 → #0a0e1f) contient son propre défilement : .pis-scroll en position: absolute; inset: 0; overflow-y: auto, barre masquée (scrollbar-width: none + ::-webkit-scrollbar { width: 0 }). Dedans, l'espaceur .pis-track fait 320 % de la hauteur, et .pis-stage, en position: sticky; top: 0, mesure 31.25% de l'espaceur — exactement la hauteur du défileur (0,3125 × 3,2 = 1) : la scène reste épinglée pendant toute la course, qui vaut 2,2 hauteurs de zone. Mesuré à 1 280 px de large : zone de 1 264 × 948 px, espaceur de 3 034 px, 2 086 px de course ; à 400 px : zone de 384 × 288, course de 634 px. Au centre, <model-viewer class="pis-mv">240 × 240 px fixes, arrondi 16 px, fond dégradé, --poster-color: transparent — avec camera-orbit="0deg 75deg 105%", field-of-view="32deg", exposure="1.1", shadow-intensity="1", interaction-prompt="none", disable-zoom, disable-tap, reveal="auto" et un src qui pointe vers le modèle de démonstration servi par jsDelivr ; dessous, la légende .pis-cap (« angle 0° », 12 px, #aeb2c8) et, en haut, l'indication .pis-hint (« Scrollez pour faire tourner », pastille rgba(0,0,0,.4), z-index: 4). Légende, indication et viewer sont en pointer-events: none : la molette traverse tout et atteint le défileur.

La bibliothèque arrive par une balise module placée en tête du HTML : model-viewer 3.5.0 depuis Google Hosted Libraries (ajax.googleapis.com) — 935 194 octets, 254 Ko compressés sur le fil, three.js r163 embarqué. Un module ES est différé par nature : il ne bloque pas l'affichage, et l'élément <model-viewer>, déjà dans le DOM, est mis à niveau dès que sa définition arrive (mesuré : customElements.whenDefined résolu entre 170 et 810 ms). reveal="auto" lance alors la requête du modèle : SunglassesKhronos.glb, 371 188 octets (277 Ko compressés), servi par jsDelivr depuis le dépôt d'exemples Khronos et figé sur un commit — huit maillages, 13 396 triangles, une texture PNG et cinq matériaux PBR (branches, crochets, plaquettes, deux couches de verre en KHR_materials_transmission, volume, ior et iridescence), sous licence CC BY 4.0 (Eric Chadwick, Darmstadt Graphics Group). Mesuré : 1,6 à 2,3 s entre le chargement de la page et l'événement load du viewer, fichiers servis par les CDN. C'est un modèle de démonstration : remplacez-le par le vôtre. Ensuite update(), posé sur scroll (passive) et resize : p = scrollTop / (scrollHeight − clientHeight), a = Math.round(p × 360), puis mv.setAttribute('camera-orbit', a + 'deg 75deg 105%') — l'azimut suit le défilement, l'élévation (75°) et la distance (105 % du cadrage automatique) sont figées dans la chaîne. La légende reçoit a, l'indication passe à opacity: 0 dès p > 0.04 (84 px mesurés). Un événement, une écriture : 9 événements scroll ont produit 9 mutations de l'attribut. Un cran de molette de 100 px vaut 17° ; 21 crans font le tour à 1 264 px de large, 7 à 384 px.

Le lissage n'est pas dans le code : camera-orbit fixe une cible que la caméra de model-viewer rejoint selon interpolation-decay (50 ms par défaut, attribut absent). Mesuré après un cran : à 5° de la cible en 123 ms, 1° en 231 ms, 0,1° en 364 ms ; après un saut de 0 à 180° : 10° en 238 ms, 1° en 377 ms. Et la caméra prend le chemin le plus court : un saut de 0 à 190° passe par 357,9° (elle tourne à l'envers), 0 → 324° se fait en −36°. Invisible à la molette, visible quand on tire l'ascenseur d'un coup ou d'un geste rapide au trackpad. En fin de course (2 086 px), 360° est la vue de départ et le cran suivant fait défiler la page (chaînage mesuré : scrollY 100). Sous prefers-reduced-motion, le script sort avant de poser l'écouteur — mais n'ajoute jamais la classe pis-reduced que le CSS attend pour ramener l'espaceur à 100 % : la piste de 2 086 px reste, l'orbite reste à 0° et « Scrollez pour faire tourner » reste affiché (mesuré sur 8 crans). Restes d'une variante à cube CSS : .pis-cube et --pis-a dans le JS, perspective: 820px sur la scène et la règle .pis-scene.pis-reduced .pis-cube — aucun .pis-cube dans le HTML. L'indication porte un style="opacity: 1;" figé dans le HTML, que le script réécrit.

Accessibilité

  • prefers-reduced-motion présent mais incomplet : le script lit la préférence et ne pose pas l'écouteur scroll — le modèle ne tourne plus —, mais il n'ajoute pas la classe pis-reduced prévue par le CSS (.pis-track { height: 100% }). Mesuré avec la préférence active : 2 086 px de piste vide à faire défiler, indication « Scrollez pour faire tourner » à opacity: 1 en permanence, orbite figée à 0°. Ajoutez scene.classList.add('pis-reduced') et masquez .pis-hint dans la sortie anticipée ; le modèle (1,3 Mo avec la bibliothèque, 530 Ko compressés) est chargé quand même.
  • Clavier : model-viewer expose dans son shadow DOM un div.userInput avec role="img", tabindex="0" et, pour nom, l'attribut alt (« Lunettes de soleil 3D — vue 360° ») — un seul arrêt de tabulation, mesuré. Le focus étant dans .pis-scroll, les flèches font défiler la zone : ↓ = 40 px = 7°, soit 53 pressions pour le tour à 1 264 px de large. Écrivez un alt qui décrit l'objet, c'est le seul texte annoncé : la légende d'angle et l'indication sont en aria-hidden="true" et aucune zone live ne suit l'angle — acceptable pour un visuel décoratif, insuffisant si l'angle porte une information.
  • Contraste : légende #aeb2c8 à 12 px sur le dégradé de la zone, 7,1:1 sur #1b2350 et 9,1:1 sur #0a0e1f ; indication blanche sur la pastille rgba(0,0,0,.4), 17,9:1. Les deux tombent sous 4,5:1 sur une page claire (2,1:1 et 2,85:1) : recolorez si vous retirez le dégradé. Le modèle n'a pas d'exigence de contraste, mais son alt oui.
  • Piège à molette : la zone est un défileur imbriqué qui capture la molette pendant 21 crans à 1 264 px de large (7 à 384 px) avant de rendre la main à la page (chaînage mesuré au cran 22). Un lecteur qui descend la page s'y arrête sans l'avoir voulu ; un utilisateur au trackpad peut passer les 360° en un geste. Raccourcissez la course (.pis-track { height: 200% }, .pis-stage { height: 50% }) ou sortez la zone du flux de lecture principal.
  • Intégration : la rotation est déclenchée par l'utilisateur, jamais seule ; le viewer est en pointer-events: none, donc pas de rotation à la souris ni au toucher — une seule commande, cohérente. Prévoyez l'échec : si le CDN de la bibliothèque ne répond pas, l'élément reste inconnu (carré dégradé de 240 px) ; si le modèle est injoignable, ou hébergé sur un domaine qui n'autorise pas le CORS, model-viewer émet error (type: 'loadfailure', mesuré) et n'affiche rien — écoutez-le et affichez une image de repli.

Compatibilité navigateur

Requiert les modules ES (type="module"), les custom elements, position: sticky, aspect-ratio, inset et WebGL 2 (three.js r163 embarqué dans model-viewer 3.5.0) ; le modèle de démonstration utilise KHR_materials_transmission, volume, ior et iridescence, pris en charge par cette version. Le pilotage est en JavaScript classique (événement scroll + attribut) : il fonctionne dans Firefox, contrairement aux animations CSS en animation-timeline, que Firefox ne prend en charge dans aucune version. Deux ressources externes, servies avec Access-Control-Allow-Origin: * : la bibliothèque model-viewer (935 Ko, ajax.googleapis.com) et le modèle de démonstration (371 Ko, cdn.jsdelivr.net) — à remplacer par le vôtre.

Chrome 88+✓ Complet
Firefox 89+✓ Complet
Safari 15+✓ Complet (WebGL 2 et aspect-ratio depuis 15)
Edge 88+✓ Complet
Mobile iOS 15+✓ Complet (défilement au doigt ; 1,3 Mo à télécharger, 530 Ko compressés)
Android Chrome✓ Complet (défilement au doigt ; 1,3 Mo à télécharger, 530 Ko compressés)

Sans JavaScript, ni le module ni le script ne s'exécutent : la zone montre le carré dégradé de 240 px, « angle 0° » et l'indication, et 2,2 hauteurs de piste vide défilent quand même. Sans WebGL 2, model-viewer émet error et le carré reste vide. Si un CDN est bloqué (CSP sans ajax.googleapis.com ou cdn.jsdelivr.net, réseau filtré), même carré vide : hébergez alors les deux fichiers avec la page. Sous prefers-reduced-motion : modèle chargé, figé à 0°, indication affichée (voir accessibilité).

Le code

Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :

index.html — structure
<!-- bibliothèque model-viewer 3.5.0, chargée depuis le CDN Google -->
<script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.5.0/model-viewer.min.js" integrity="sha384-Ftcjj/GNLxPvzNDftO/oryXB9aGxsGZY9JGqsXG0uUKgQDl9RfDgsx9NJ/4IVNPe" crossorigin="anonymous"></script>
<div class="pis-scene">
  <div class="pis-hint" aria-hidden="true">Scrollez pour faire tourner</div>
  <div class="pis-scroll">
    <div class="pis-track">
      <div class="pis-stage">
        <!-- src : modèle de démonstration, à remplacer par le vôtre (CORS requis s'il est sur un autre domaine) -->
        <model-viewer class="pis-mv" alt="Lunettes de soleil 3D — vue 360°" camera-orbit="0deg 75deg 105%" field-of-view="32deg" interaction-prompt="none" disable-zoom disable-tap shadow-intensity="1" exposure="1.1" reveal="auto" src="https://cdn.jsdelivr.net/gh/KhronosGroup/glTF-Sample-Assets@34eb677041d54bf92b435f1bf2671b391d4a3892/Models/SunglassesKhronos/glTF-Binary/SunglassesKhronos.glb"></model-viewer>
        <div class="pis-cap" aria-hidden="true">angle <b><span class="pis-deg">0</span</b></div>
      </div>
    </div>
  </div>
</div>
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
src du viewer (HTML — <model-viewer>) Lunettes de démonstration (SunglassesKhronos.glb, 371 Ko, jsDelivr) N'importe quel glTF/GLB, hébergé avec la page (même origine : rien à configurer) ou sur un domaine qui envoie Access-Control-Allow-Origin — model-viewer le lit par fetch(), et sans cet en-tête le viewer émet error. Le poids commande l'attente : 1,6 à 2,3 s mesurées pour 371 Ko + 935 Ko de bibliothèque via les CDN.
Course (CSS — .pis-track { height: 320% }) 320 % (2,2 hauteurs) Longueur de défilement pour un tour : 2 086 px à 1 264 px de large, 634 px à 384 px. 200% = tour en 10 crans, mais .pis-stage doit rester égal au défileur : passez-le de 31.25% à 50%, sinon l'épinglage décroche.
Amplitude (JS — Math.round(p * 360)) 360° 180 = un demi-tour, 720 = deux tours. Au-delà de 180° dans un seul saut, la caméra part dans l'autre sens (chemin le plus court).
Élévation et distance (HTML — camera-orbit="0deg 75deg 105%" ; JS — a + 'deg 75deg 105%') 75° / 105 % Deux endroits à changer ensemble : la chaîne du JS écrase l'attribut à chaque défilement. 90deg = vue de profil, 60deg = plus plongeante ; 120% éloigne la caméra.
Lissage (HTML — interpolation-decay) absent = 50 ms Retard de la caméra sur la cible : 1° en 231 ms par défaut ; 20 = 91 ms, 0 = collée au défilement (9 ms), 200 = 0,9 s de traîne (mesurés).
Taille du viewer (CSS — .pis-mv { width: 240px; height: 240px }) 240 × 240 px Fixe quelle que soit la zone : 19 % de la largeur à 1 264 px, 83 % de la hauteur à 288 px. Passez en min(60%, 60vh) pour suivre le conteneur ; le 105% de camera-orbit recadre automatiquement.
Rendu (HTML — exposure, shadow-intensity, field-of-view) 1.1 / 1 / 32° Luminosité, ombre portée au sol, focale. exposure="0.8" assombrit un modèle blanc ; field-of-view="20deg" aplatit la perspective.
Seuil de l'indication (JS — p > 0.04) 4 % (84 px) L'indication disparaît après 4 % de course. Posée en left: 50%, elle ne dispose que de la moitié de la zone : mesurée sur deux lignes dès 324 px de large — ajoutez white-space: nowrap.
Chargement de la bibliothèque (HTML — balise module en tête du code) avec la page (module différé, 935 Ko) Le module ne bloque pas l'affichage, mais il part avec la page. Pour ne le télécharger qu'à l'approche de la zone, créez la balise depuis un IntersectionObserver (rootMargin: '300px') : l'élément déjà dans le DOM est mis à niveau à l'arrivée du module. Pour ne dépendre d'aucun CDN, npm i @google/model-viewer et pointez le src de la balise vers votre copie.

FAQ

Il n'y en a pas. Le code affiche un modèle glTF (ici des lunettes de démonstration, 13 396 triangles, verres en transmission et iridescence) dans <model-viewer> et fait tourner sa caméra : la géométrie est réelle, lumière et ombre sont calculées à chaque angle. C'est ce qui le sépare de son voisin Image Sequence (fx-0583), qui commute 24 états CSS (dégradé, arrondi, taille) au fil d'un défilement de 920 px, sans image non plus — un simulacre de séquence. Ici on peut lire une charnière ou un filetage ; là-bas, non. Pour de vraies photos 360°, ce n'est pas le bon effet : model-viewer ne lit pas de série d'images.
Le HTML commence par une balise module qui charge model-viewer 3.5.0 depuis Google Hosted Libraries (ajax.googleapis.com, 935 Ko, 254 Ko compressés) ; le src du viewer pointe vers un modèle de démonstration du dépôt d'exemples Khronos servi par jsDelivr (371 Ko, CC BY 4.0, URL figée sur un commit : elle ne bougera pas). Mesuré sur une page vierge : les deux répondent 200 avec Access-Control-Allow-Origin: *, et le viewer émet load entre 1,6 et 2,3 s. Pour votre modèle : hébergez le .glb à côté de la page (même origine, rien à configurer) ou sur un domaine qui envoie cet en-tête CORS — model-viewer le lit par fetch(), et sans l'en-tête la requête est refusée : le viewer émet error (loadfailure) et le carré reste vide. Pour la bibliothèque : npm i @google/model-viewer, copiez model-viewer.min.js chez vous et changez le src de la balise — plus aucune dépendance externe. Tant que vous gardez les CDN, une Content-Security-Policy doit autoriser ajax.googleapis.com en script-src et cdn.jsdelivr.net en connect-src.
Parce que le code écrit une cible : camera-orbit est interpolé par model-viewer avec interpolation-decay à 50 ms par défaut — mesuré, un cran de 17° est atteint à 1° près en 231 ms, un saut de 180° en 377 ms. Et l'interpolation choisit le chemin le plus court : demandez 190° depuis 0° et la caméra passe par 357,9°, en sens inverse. À la molette (17° par cran) on ne le voit jamais ; en tirant l'ascenseur ou d'un geste rapide au trackpad, si. interpolation-decay="0" colle la caméra au défilement (9 ms mesurés) ; pour interdire le retour arrière, cumulez l'angle sans le ramener sous 360 — model-viewer accepte les valeurs au-delà.