Utiliser les Scroll Driven Animations en CSS comme indicateur de scroll
lundi 04 décembre 2023
Lâautre jour je commençais Ă vous parler des Scroll Driven Animations en animant un header sticky. Aujourdâhui on va continuer Ă explorer cette nouvelle techno CSS en lâutilisant comme indicateur.
Notamment, jâai un truc qui mâembĂȘte rĂ©guliĂšrement sur le web : comment est-ce que je suis censĂ© savoir quâun contenu est scrollable ou non. Câest un problĂšme dâautant plus rĂ©current que les scrollbars ont tendance Ă disparaĂźtre, Ă©tant donnĂ© que sur mobile on cherche Ă tout prix Ă Ă©conomiser de la place.
Pour rĂ©soudre ce problĂšme, jâai remarquĂ© 3 types de solutions :
-
Lâicone mystĂ©rieux : un petit icone, parfois animĂ© pour pousser les gens Ă regarder plus loin
-
Le boucher du web : on sâassure que le contenu suivant est partiellement visible pour indiquer quâil y a en a plus
-
Le dégradé mystérieux : on gÚne légÚrement la lecture avec un petit dégradé pour pousser à aller voir la suite sans avoir une coupure trop nette
Aujourdâhui on va se concentrer sur cette 3Ăšme solution pour laquelle les Scroll Driven Animations semblent ĂȘtre la solution idĂ©ale : uniquement en CSS et trĂšs fluide Ă©tant donnĂ© que ce nâest plus exĂ©cutĂ© sur le main thread.
PrĂ©requis : Avant de lire cet article, je vous conseille de lire le paragraphe Comment fonctionne une Scroll Driven Animation : il prĂ©sente le concept et pose les bases pour avoir un modĂšle mental de comment ça fonctionne. Seul ce paragraphe est vraiment obligatoire. Le reste de lâarticle (bien que trĂšs intĂ©ressant đ) nâest pas nĂ©cessaire.
Attention : A ce jour, ce que je vais vous prĂ©senter nâest disponible que sur les derniĂšres versions de Chrome (caniuse). Evitez donc de lâutiliser en production pour le moment ou Ă minima, pensez Ă utiliser
@supports (animation-timeline: view()) {}
pour éviter des mauvaises surprises sur les autres navigateurs.
TL;DR
Vous pouvez accéder au code et à la démo ici : https://codepen.io/julienpradet/pen/gOqObbW
Lâobjectif est triple ici :
- animer lâopacitĂ© de chaque Ă©lĂ©ment en fonction de sa position dans le scroll
- ne pas rĂ©duire lâopacitĂ© si on est tout en haut du scroll : Ă©tant donnĂ© que lâopacitĂ© rĂ©duite indique le reste Ă scroll, on veut quâelle soit Ă 100% si on est tout en haut.
- idem quand on est tout en bas du scroll
Vous constaterez par ailleurs que je nâai pas utilisĂ© de dĂ©gradĂ© et que je me suis contentĂ© dâopacitĂ© parce que ça mâa eu lâair suffisant et plus joli. Mais le code pourrait ĂȘtre adaptĂ© si vous prĂ©fĂ©rez un dĂ©gradĂ©.
0. Structure générale de notre HTML
Pour cet exercice, nous faire un exemple minimaliste de chat avec une boĂźte qui est scrollable verticalement, et Ă lâintĂ©rieur de celle-ci une liste de messages.
<div class="scroll-container">
<p class="message me">Hey!</p>
<p class="message you">Hey!</p>
<p class="message me">What's up?</p>
<p class="message you">All good, you?</p>
<p class="message me">All good.</p>
</div>
.scroll-container {
max-height: 500px;
overflow-y: auto;
}
.message {
padding: 1rem;
background: var(--color-primary);
}
1. DĂ©finir lâopacitĂ© en fonction de la position dâun Ă©lĂ©ment
Une fois la structure de base prise en compte, nous allons commencer Ă faire changer lâopacitĂ© en fonction de la position de lâĂ©lĂ©ment dans lâĂ©cran.
.message {
animation: y-distribution; /* 2 */
animation-timeline: view(); /* 1 */
}
@keyframes y-distribution {
0% {
opacity: 0.3; /* 3 */
}
25% /* 4 */ {
opacity: 1;
}
/* 5 */
75% {
opacity: 1;
}
100% {
opacity: 0.3;
}
}
animation-timeline
permet de savoir quel genre dâanimation on souhaite effectuer. Quand on pense Scroll Driven Animation, on va avoir tendance Ă penser Ăanimation-timeline: scroll()
qui permet de dire : si on est tout en haut du scroll, lâanimation est Ă 0%, et tout en bas, Ă 100%. Mais ici, ce qui nous intĂ©resse ce nâest pas lâopacitĂ© du premier et du dernier Ă©lĂ©ment de tout le scroll, mais uniquement le premier et le dernier Ă©lĂ©ment visible. Câest pour ça quâon utiliseanimation-timeline: view()
- on choisit quelle animation executer : en lâoccurrence ce sera
y-distribution
qui se définit avec@keyframes
comme pour une animation normale. - on ne veut pas complÚtement cacher le premier élément, donc on part de 0.3 quand il est tout en haut de la partie visible
- Puis on le rend totalement visible Ă partir de 25%. Ce pourcentage dĂ©pend de la hauteur de votre scroll-container : sâil est trop bas, vous risquez de tomber dans des situations oĂč lâopacitĂ© nâest pas du tout appliquĂ©eâŻ; sâil est trop haut, tous vos messages seront en opacitĂ© rĂ©duite.
- Puis on refait la mĂȘme chose dans le sens inverse : plus on est proche du bas, plus lâopacitĂ© est rĂ©duite
Ca fonctionne Ă merveille, mais on a pour lâinstant juste une animation dâentrĂ©e et de sortie : si on est tout en haut, le premier message est Ă 0.3 dâopacitĂ© et donc peu lisible.
2. Prendre en compte la proximité avec le haut du scroll
Nous allons donc devoir adapter notre CSS pour sâassurer que lâopacitĂ© est de 1 quand on est tout en haut du scroll.
Le CSS pourrait alors ressembler à ça :
@property --min-opacity {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@keyframes top-proximity; {
0% {
--min-opacity: 1; /* 3 */
}
100% {
--min-opacity: 0;
}
}
.message {
animation: top-proximity;
animation-timeline: scroll(); /* 1 */
animation-range: 0% 3rem; /* 2 */
}
- Ce qui nous intĂ©resse câest la hauteur totale du scroll, donc on repasse Ă une
animation-timeline: scroll()
- On ne peut plus faire notre
@keyframes
sur toute la hauteur du scroll : sinon, quand on a 100 messages, lâanimation met trop de temps Ă se jouer sur le scroll. On ajoute donc uneanimation-range: 0% 3rem
pour indiquer que lâanimation se jouera uniquement sur 3rem de hauteur. Ainsi, lâanimation est Ă 0% si on est tout en haut, 50% si on a scrollĂ© 1.5rem, 100% si on a scrollĂ© 3rem et plus. - Pendant lâanimation, on va faire Ă©voluer la propriĂ©tĂ©
--min-opacity
 : si on est tout en haut, alors on ne devrait pas diminuer lâopacitĂ© en lui dĂ©finissant un minimum de 1âŻ; sinon, elle peut Ă©voluer normalement (donc pas de minimum en la mettant Ă 0) - Pour que cette propriĂ©tĂ© CSS soit animable, on doit dĂ©finir au navigateur comment lâanimer : on anime pas de la mĂȘme façon un nombre et une couleur. Câest pour cette raison quâon aide le navigateur en dĂ©finissant une
@property
Câest bien beau tout ça, mais si on met Ă jour notre code, ça ne marche plus : en effet, on a retirĂ© lâanimation initiale et le --min-opacity
nâest utilisĂ©e nulle part.
Nous allons donc devoir combiner les 2 méthodes ensemble. Pour ce faire, nous pouvons chaßner les animations dans animation-timeline
et animation-range
en les séparant par des virgules.
/* 1 */
@property --opacity {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@property --min-opacity {
syntax: "<number>";
inherits: true;
initial-value: 1;
}
@keyframes top-proximity; {
0% { --min-opacity: 1; }
100% { --min-opacity: 0; }
}
@keyframes y-distribution {
0% { --opacity: 0.3; }
25% { --opacity: 1; }
75% { --opacity: 1; }
100% { --opacity: 0.3; }
}
.message {
animation:
y-distribution,
top-proximity;
animation-timeline:
view(),
scroll();
animation-range:
normal,
0% 3rem;
/* 2 */
opacity: max(--min-opacity, --opacity);
}
- PlutĂŽt que dâanimer lâ
opacity
dans lâanimationy-distribution
on va prĂ©fĂ©rer mettre Ă jour une nouvelle @property quâon va appeler--opacity
- Cela nous permet ainsi de calculer lâ
opacity
finale Ă partir de--min-opacity
et--opacity
Cette fois-ci, le résultat est meilleur : si on est tout en haut le premier message est entiÚrement visible. Si on commence à scroller, les messages du haut voient leur opacité diminuer.
3. Prendre en compte la proximité avec le bas du scroll
Cependant, nous avons un problĂšme : quand on est tout en haut, le dernier message visible est lui aussi complĂštement visible. Câest un problĂšme parce quâau tout dĂ©but du scroll, on ne voit plus quâil y a une suite.
Nous allons donc voir comment utiliser la proximitĂ© avec le haut et avec le bas. Pour cela, nous allons devoir reproduire ce quâon a fait pour le top-proximity
mais cette fois ci avec un bottom-proximity
.
@keyframes top-proximity; {
0% { --min-opacity: 1; }
100% { --min-opacity: 0; }
}
@keyframes bottom-proximity; {
0% { --min-opacity: 1; }
100% { --min-opacity: 0; }
}
@keyframes y-distribution {
0% { --opacity: 0.3; }
25% { --opacity: 1; }
75% { --opacity: 1; }
100% { --opacity: 0.3; }
}
Cependant, top-proximity
et bottom-proximity
rentrent en conflit, on ne sait pas vraiment lequel doit gagner. On va donc plutĂŽt les renommer en --top-min-opacity
et --bottom-min-opacity
et en fonction de oĂč on est dans le scroll, on va utiliser soit lâun, soit lâautre.
Si nous étions en JavaScript, nous pourrions faire une condition qui ressemblerait à  :
.message {
opacity: max(
var(--opacity),
/* Ceci ne fonctionne pas */
var(--is-top) ? var(--top-min-opacity): var(--bottom-min-opacity)
);
}
Mais nous ne sommes pas en JS. Donc comment transformer ça en utilisant uniquement des calculs�
Lâastuce est de constater quâune min-opacity
quâon veut ignorer devrait ĂȘtre Ă 0
. En effet, max(0, var(--opacity)) === var(--opacity)
.
De fait, si on veut ignorer --top-min-opacity
quand on est dans la partie basse de lâĂ©cran, il faut alors la multiplier par 0
. Si on est dans la partie haute, on peut la conserver Ă sa valeur initiale et donc la multipler par 1
.
Ceci nous donne : --min-opacity: calc(var(--is-top) * var(--min-top-opacity))
avec --is-top: 0
dans la partie haute, et --is-top: 1
dans la partie basse.
Conceptuellement câest exactement comme si nous avions Ă©crit : --min-opacity: var(--is-top)? var(--top-min-opacity): 0
.
Inversement, si on est en haut on devrait multiplier --min-bottom-opacity
par 0
, et par 1
si on est en bas. Autrement dit, on peut multiplier par lâinverse: 1 - var(--is-top)
.
Ce qui nous donne: --min-opacity: calc((1 - var(--is-top)) * var(--min-bottom-opacity))
Enfin, pour prendre les deux en compte en mĂȘme temps, on peut faire la somme des 2Â : quand --is-top
vaudra 0
, câest le --min-bottom-opacity
qui gagnera, quand il vaudra 1
câest le --min-top-opacity
.
Ca nous donne la formule suivante :
.message {
--min-opacity: var(--is-top) * var(--top-min-opacity)
+ (1 - var(--is-top)) * var(--bottom-min-opacity);
opacity: max(var(--opacity), var(--min-opacity));
}
đĄ Astuce : La bonne nouvelle câest que cette mĂ©thode marche avec un nombre infini dâindicateurs : en effet, plutĂŽt que de faire
1 - var(--is-top)
vous auriez pu créer un nouveau--is-bottom
. Donc en appliquant cette technique, vous pourriez aller jusquâĂ implĂ©menter un switch en CSS.
Maintenant la question est : comment définir ce --is-top
� En réutilisant nos @keyframes
 :
+@property --is-top {
+ syntax: "<number>";
+ inherits: true;
+ initial-value: 1;
+}
@keyframes y-distribution {
0% { --opacity: 0.3; }
25% { --opacity: 1; }
+ 50% { --is-top: 1; }
+ 51% { --is-top: 0; }
75% { --opacity: 1; }
100% { --opacity: 0.3; }
}
Et voila! Nous avons maintenant un comportement de scroll qui :
- diminue lâopacitĂ© quand
.message
est au bord de la vue - sauf si on est tout en haut du scroll
- ou si on est tout en bas du scroll
Conclusion
Ainsi, on sait toujours quand il y a plus de contenu cacher derriĂšre le scroll et surtout câest trĂšs lĂ©ger pour le navigateur vu que câest fait uniquement en CSS. Attention toutefois Ă garder en tĂȘte que ce nâest disponible que sur les Chromium pour lâinstant, mais que ce standard se rĂ©pandre au fur et Ă mesure.
Si vous en avez besoin dĂšs aujourdâhui, sĂ»rement quâune alternative JS sera plus pertinente. (ex: GSAP)
Et vous, vous auriez fait commentâŻ? Je serais curieux dâen savoir plus.
En tout cas, si câest le genre de choses qui vous intĂ©ressent, nâhĂ©sitez pas Ă me suivre et Ă partager autour de vous. Je publie rĂ©guliĂšrement des articles autour de la performance et du dĂ©veloppement web. A trĂšs vite đ
Si vous voulez suivre mes publications, il paraĂźt que j'ai un feed RSS, Mastodon et un Twitter.
Si vous pensez à d'autres méthodes que vous voudriez que je mette en place (pigeon voyageur, avion en papier, etc.), n'hésitez pas à me les proposer :)