Animations pilotées par le défilement : animation-timeline plutôt qu'IntersectionObserver
Animations pilotées par le défilement : animation-timeline plutôt qu'IntersectionObserver
Le reveal-on-scroll classique se passe ainsi : créer un IntersectionObserver, observer une liste d'éléments, et quand l'un d'eux franchit le seuil, ajouter une classe .is-visible qui déclenche une transition CSS. La barre de progression est pire : un listener scroll qui recalcule scrollTop / (scrollHeight - clientHeight) et l'écrit dans style.width. Les deux fonctionnent. Les deux ont le défaut dont parlait l'article sur l'INP – la logique vit sur le thread principal.
Mais il y a un second problème avec la version JavaScript, et il est le plus intéressant, car aucune optimisation ne peut le corriger.
Pourquoi le parallax JS semble toujours légèrement décalé
Les navigateurs modernes font défiler sur le compositeur. Le défilement lui-même n'attend pas JavaScript – le compositeur déplace le contenu déjà peint et montre des frames, rapidement, indépendamment de ce que fait le thread principal.
L'événement scroll est cependant livré à JavaScript après que c'est déjà arrivé. La séquence est donc : le navigateur défile et peint, puis informe le listener de la nouvelle position. Votre couche réagit toujours à une position de défilement que l'utilisateur a déjà vue. C'est exactement pourquoi position: sticky existe.
Les scroll-driven animations coupent le problème à la racine. C'est la même animation CSS qu'on connaît, avec une substitution : au lieu que le temps pilote la progression de 0% à 100%, c'est la position d'un conteneur de défilement. Le navigateur peut l'exécuter sur le compositeur.
Une barre de progression en quatre lignes
css
.progress {position: fixed;inset: 0 0 auto 0;height: 4px;transform-origin: left;animation: grow linear both;animation-timeline: scroll(root block);}@keyframes grow {from { transform: scaleX(0); }to { transform: scaleX(1); }}
Trois choix délibérés : linear car tout autre easing décalerait la barre par rapport à la position de lecture réelle. Pas de durée car la durée n'a aucun sens sur une timeline de défilement. Et scroll(root block) pour nommer le document explicitement.
Reveal on scroll : la timeline view()
css
.reveal {animation: fade-up linear both;animation-timeline: view();animation-range: entry 0% entry 60%;}@keyframes fade-up {from { opacity: 0; transform: translateY(32px); }to { opacity: 1; transform: translateY(0); }}
Les quatre ranges nommés valent la peine d'être mémorisés :
entry– pour tout ce qui doit arriver.exit– pour faire disparaître les choses en partant.cover– tout le voyage de l'élément. La valeur par défaut.contain– uniquement la partie où l'élément est entièrement visible.
Le piège qui coûte une heure : le raccourci animation réinitialise la timeline
animation est un raccourci, et comme tout raccourci CSS, il réinitialise toutes ses propriétés longues à leurs valeurs initiales – y compris animation-timeline et animation-range. Donc ceci ne fait absolument rien :
css
/* CASSÉ : le raccourci réinitialise animation-timeline à auto */.reveal {animation-timeline: view();animation-range: entry 0% entry 60%;animation: fade-up linear both;}
La correction est simplement une question d'ordre :
css
/* Correct : timeline et range après le raccourci */.reveal {animation: fade-up linear both;animation-timeline: view();animation-range: entry 0% entry 60%;}
La règle : sur une animation pilotée par le défilement, animation-timeline et animation-range sont toujours les deux dernières déclarations dans le bloc.
Timelines nommées : animer une chose en fonction d'une autre
css
.gallery {overflow-x: auto;scroll-timeline: --gallery inline;}.gallery-indicator {animation: fill linear both;animation-timeline: --gallery;timeline-scope: --gallery;}
timeline-scope remonte le nom suffisamment haut dans l'arbre pour que les frères et sœurs et les parents éloignés puissent le voir.
L'amélioration progressive n'est pas optionnelle ici
css
.reveal {opacity: 1; /* par défaut : visible partout */}@supports (animation-timeline: view()) {@media (prefers-reduced-motion: no-preference) {.reveal {animation: fade-up linear both;animation-timeline: view();animation-range: entry 0% entry 60%;}}}
Pièges et bonnes pratiques
- Animer uniquement
transformetopacity. - Ne jamais cacher du contenu qu'une animation peut seulement révéler. L'état de base doit être l'état visible.
- « Pourquoi mon animation est-elle bloquée ? » c'est presque toujours le conteneur de défilement.
overflow: hiddensur un wrapper crée un conteneur de défilement qui ne défile jamais. - Utiliser
bothcomme fill mode. - Le reduced motion est un paramètre médical, pas une politesse.
- Ne pas supprimer le JavaScript dont on a encore besoin.
Conclusion
Les scroll-driven animations appliquent l'idée du thread principal à toute une catégorie d'effets : ce qui nécessitait un listener de défilement, un observer, un basculement de classe et une bibliothèque comme ScrollTrigger devient une seule propriété CSS.