8min.

Scroll-driven animations en CSS : guide pratique pour s’affranchir du JavaScript

Pour celles et ceux qui ont déjà réalisé des animations au défilement de la page vous avez sûrement utilisé par le passé des librairies comme scroll reveal ou animate on scroll. Moi-même, j’ai déjà passé de longues minutes sur ces sites à jouer avec ma barre de défilement (oui bon, chacun ses occupations…).

Peut-être avez-vous par la suite écrit du code vanilla maison ou utilisé l’API Intersection Observer pour déclencher une animation lorsqu’un élément entre dans le viewport. On s’est alors débarrassé des dépendances externes, mais pas du JavaScript.

Bonne nouvelle : il est désormais possible de réaliser ces animations sans écrire une seule ligne de JS. C’est ce que l’on va découvrir avec les scroll-driven animations en CSS.

Section intitulée pourquoi-utiliser-les-scroll-driven-animationsPourquoi utiliser les scroll-driven animations

Cela fait dix ans que les animations au scroll ont été proposées dans la spécification CSS. Après plus de cinq ans de développement, elles commencent enfin à être utilisables en production. Les scroll-driven animations (que l’on pourrait traduire en français par « animations pilotées par le défilement »), permettent d’animer un élément pendant le défilement. À ne pas confondre avec les scroll-triggered animations, qui se déclenchent à un moment donné du défilement (par exemple, quand un élément entre dans le viewport) et se jouent ensuite jusqu’au bout, sans interaction avec le scroll.

Le véritable avantage des scroll-driven animations se situe au niveau de la performance. Lorsque l’on réagit aux évènements du scroll en JavaScript on s’appuie sur le fil d’exécution principal. Si celui-ci est bloqué, les animations risquent de saccader.

Avec les scroll-driven animations, on utilise les capacités natives du CSS et de l’API Web Animations, ce qui permet de créer des animations fluides et performantes. Voyons maintenant comment les mettre en place concrètement dans nos projets.

Section intitulée une-premiere-animation-avec-les-scroll-timelineUne première animation avec les scroll timeline

Vous voyez l’indicateur de progression en haut de page ? Sans surprise, il a été développé à l’aide de JavaScript en écoutant l’événement scroll. Si on voulait créer cette animation en CSS voici ce qu’on écrirait :

@keyframes expand {
	from {
		transform: scaleX(0);
	}
	to {
		transform: scaleX(1);
	}
}

.progress {
	animation: expand linear 1s forwards;
}

Par défaut, cette animation s’exécute dès le chargement de la page, car elle utilise la timeline du document. Pour qu’elle évolue en fonction du scroll, il suffit de changer la timeline de l’animation en utilisant la propriété CSS animation-timeline combinée à la fonction scroll() :

.progress {
	/*
		On indique "auto" pour la durée.
		Ce n'est pas indispensable mais plus explicite
	*/
	animation: expand linear auto forwards;
	/* On ajoute la propriété animation-timeline */
	animation-timeline: scroll();
}

Et voilà 🎉 c’est aussi simple que ça.

La fonction scroll() prend 2 éléments en paramètre :

  • l’axe de défilement : block (par défaut), inline, x ou y. Je vous encourage à utiliser les propriétés logiques ;
  • le conteneur scrollable : nearest (le plus proche, par défaut), root (le document) ou self (l’élément lui-même).

Vous pouvez tester les différentes valeurs possibles grâce à cet outil développé par Bramus Van Damme, développeur chez Google.

Dans notre cas, nous n’avons pas besoin de préciser de paramètres : les valeurs par défaut (block, nearest) conviennent, puisque le conteneur scrollable le plus proche est le document.

Notre animation a toutefois un petit défaut : si notre page dispose d’un footer ou d’éléments en bas de page (comme c’est le cas ici), l’animation de progression atteindra 100% en bas de la page et non en bas de l’article. Pour corriger cela on peut utiliser la propriété CSS animation-range, qui permet de définir la portion du scroll sur laquelle l’animation doit se jouer. On pourrait par exemple indiquer quelque chose comme ceci :

.progress {
	animation: expand linear auto forwards;
	animation-timeline: scroll();
	/*
		On peut indiquer des valeurs absolues en px,
		des pourcentages, ou même utiliser calc
	*/
	animation-range: 0vh calc(100vh - var(--footer-height));
}

À noter que animation-range est un raccourci pour les propriétés animation-range-start et animation-range-end.

Section intitulée allons-plus-loin-avec-les-view-timelinesAllons plus loin avec les view-timelines

Pour l’instant, on a animé un élément en fonction du scroll global de la page. Mais il est aussi possible d’animer un élément en fonction de sa propre position dans le viewport. Pour cela, on utilise les view-timelines et la fonction view(). Celle-ci prends en paramètre uniquement l’axe de défilement : block (par défaut), inline, x ou y.

Imaginons que l’on veuille ajouter une animation sur les images d’un article du blog. On va créer une animation qui révèle un élément de gauche à droite à l’aide de clip-path et appliquer cette animation à notre élément.

@keyframes reveal {
  from {
    clip-path: inset(0 100% 0 0);
  }

  to {
    clip-path: inset(0);
  }
}

.image {
	animation: reveal linear auto forwards;
	animation-timeline: view();
}

Voici ce que l’on obtient :

C’est pas mal mais on a un problème principal : l’animation est complète lorsque l’élément quitte le viewport en haut de page. Pour corriger cela on va pouvoir à nouveau utiliser la propriété animation-range.

On va pouvoir utiliser quatre mot-clés différents associés (ou non) avec des valeurs relatives ou absolues :

  • entry : début de l’entrée dans le viewport (0%) → complètement visible (100%)
  • exit : début de la sortie (0%) → complètement sorti (100%)
  • contain : élément complètement entré (0%) → complètement sur le point de sortir (100%)
  • cover : début d’entrée (0%) → complètement sorti (100%)

Voici quelques exemples de valeurs possibles :

animation-range: cover; /* Equivalent à cover 0% cover 100% */
animation-range: entry 10% exit; /* Equivalent à entry 10% exit 100% */
animation-range: entry 10px exit 100px;
animation-range: normal /* Equivalent à cover 0% cover 100% ou juste cover */
/* Ces deux valeurs sont également équivalentes : */
animation-range: cover;
animation-range: entry exit;

Pour tester les différentes valeurs, rien de mieux que l’outil de Bramus Van Damme (je n’ai pas fini de parler de lui).

D’ailleurs… je vous ai menti, il existe aussi deux autres mots clés :entry-crossing et exit-crossing. Ceux-ci diffèrent de entry et exit uniquement si l’élément que l’on anime est plus grand que l’élément que l’on scrolle. Dans ce cas entry 100% se déclenche quand l’élément commence à quitter le conteneur alors que entry-crossing 100% correspond au moment où le bas de l’élément dépasse le bas du conteneur. Ce n’est pas assez clair ? Faites le test avec l’outil, vous verrez la différence en action.

Dans notre cas on aimerait que l’animation commence quand l’image entre dans le viewport puis soit entièrement visible quand celle-ci est environ à 30% du bas de la page.

.image {
	animation: reveal linear auto forwards;
	animation-timeline: view();
	animation-range: entry cover 30%;
}

Et voici le résultat ✨

Section intitulée strong-utiliser-des-timelines-nommees-pour-plus-de-controle-strongUtiliser des timelines nommées pour plus de contrôle

Jusqu’ici, nous avons utilisé des scroll-timelines et des view-timelines anonymes. Mais il est également possible de créer des timelines nommées (named timelines). Prenons un exemple : imaginons que l’on souhaite animer un élément en fonction du scroll d’un conteneur qui n’est ni le plus proche ascendant scrollable, ni le document. Dans ce cas on utilisera les propriétés css scroll-timeline-name et scroll-timeline-axis sur ledit conteneur.

.container {
	overflow: scroll;
	/* Le nom de la timeline doit être précédé de 2 tirets,
	Attention, cela ne signifie pas que c'est une custom property */
	scroll-timeline-name: --container;
	/* block étant la valeur par défaut, on aurait pu omettre cette propriété */
	scroll-timeline-axis: block;
}

.element {
	animation: reveal linear auto forwards;
	animation-timeline: --container;
}

À noter : scroll-timeline est un raccourci de ces deux propriétés, vous pouvez donc aussi écrire :

scroll-timeline: --container block;
/* ou même simplement : */
scroll-timeline: --container;

On peut également créer une view timeline nommée. Cela peut servir par exemple pour animer plusieurs éléments en fonction d’un autre.

Admettons que l’on veuille animer les éléments d’une section en fonction de la visibilité de la section elle-même. Il suffit de déclarer une view timeline sur celle-ci, puis de l’utiliser sur les éléments enfants.

.section {
	view-timeline: --section;
}

.text {
	animation: fade-in-up linear auto forwards;
	animation-timeline: --section;
	animation-range: entry cover 40%;
}

.image {
	animation: reveal linear auto forwards;
	animation-timeline: --section;
	animation-range: entry cover 40%;
}

Ainsi, les animations des .text et .image ne dépendent plus de leur propre entrée dans le viewport, mais bien de celle de la section.

Pour l’instant les timelines nommées ne fonctionnent que si les éléments animés sont des descendants de l’élément porteur de la timeline (scroll-timeline ou view-timeline). Pour animer un élément basé sur un autre qui n’est pas son parent, on peut utiliser la propriété timeline-scope. Appliquée plus haut dans le DOM, elle élargit le champ d’action des timelines nommées au sein de l’arbre DOM concerné.

<div class="container"> <!-- Le parent en commun -->
	<div class="scroller">...</div> <!-- L'élément que l'on va scroller -->
	<div class="element">...</div> <!-- L'élément que l'on va animer -->
</div>
.container {
	timeline-scope: --container;
}

.scroller {
	overflow: scroll;
	scroll-timeline-name: --container;
}

.element {
	animation: reveal linear auto forwards;
	animation-timeline: --container;
}

Ainsi l’élément .element s’anime en fonction de l’élément .scroller même s’ils n’ont pas de lien hiérarchique.

Section intitulée strong-support-navigateur-performance-et-accessibilite-strongSupport navigateur, performance et accessibilité

C’est toujours la même chose quand on parle de nouvelles fonctionnalités, on est toujours déçu(e)s au moment de connaître le support navigateur. Les scroll-driven animations n’y échappent pas vraiment. Si elles sont supportées par le navigateur Chrome ce n’est pas encore le cas de Safari ou Firefox.

Cependant il y a un moyen de contourner ce problème :

  • Utiliser @supports dans notre code CSS
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
  /* Scroll-Driven Animations styles */
}

Pour conserver des performances optimales, évitez d’animer des propriétés coûteuses comme top, left, height … Préférez les propriétés GPU-friendly transform et opacity au maximum.

Enfin, respectez toujours le choix de certains de vos utilisateurs et utilisatrices de désactiver les animations.

@media (prefers-reduced-motion: reduce) {
  .element {
    animation: none;
  }
}

Les scroll-driven animations représentent une belle avancée pour le CSS moderne. Encore jeunes, elles demandent quelques précautions côté support, mais offrent une vraie alternative au JavaScript pour créer des interactions fluides, performantes et maintenables.

Section intitulée pour-aller-plus-loinPour aller plus loin

Bramus Van Damme a disséqué le sujet en profondeur et propose de nombreux outils sur son site, des articles détaillés sur son blog et une série de vidéos youtube sur le sujet. C’est vraiment la personne à suivre pour approfondir le sujet.

Commentaires et discussions

Nos formations sur ce sujet

Notre expertise est aussi disponible sous forme de formations professionnelles !

Voir toutes nos formations

Ces clients ont profité de notre expertise