Comment faire un header sticky animé et performant ?
lundi 04 septembre 2023
En ce moment il y a deux nouvelles APIs qui sont en train dâarriver dans les navigateurs qui font fureur :
đĄ Pour info ces APIs sont disponibles sur Chrome uniquement pour le moment. Mais dans les 2 cas, Webkit (= Safari) et Gecko (= Firefox) sont dâaccord avec la proposition, donc ça va finir par arriver aussi.
Câest pour moi la preuve quâil y a une rĂ©elle appĂ©tence pour avoir un web plus dynamique, plus animĂ©. Et personnellement, je pense que câest lâunique raison pour laquelle, dans lâimaginaire collectif, les applications mobiles ont lâair plus quali que le web (et, accessoirement, la quantitĂ© affolante de pubs et de bandeaux de cookies).
Et preuve supplĂ©mentaire de cette appĂ©tence, jâai rarement vu autant de devs front se mettre Ă tester de nouvelles APIs, alors que ce nâest mĂȘme pas encore dispo sur tous les navigateurs.
Notamment, cette dĂ©mo de @jhey, disponible sur Codepen a vraiment attirĂ© mon attention. Les idĂ©es que jâaime beaucoup :
- animer lâavatar au moment oĂč il passe en sticky â€ïž
- rĂ©organiser le header pour quâil prenne moins de place au passage en sticky
đĄ Sticky : qui reste visible Ă lâĂ©cran quelque soit le niveau de scroll
Ce sont ces petits moments qui me font des petits guilis au ventre et me rendent heureux quand je parcours le web.
Pour construire cette dĂ©mo, il a utilisĂ© une Scroll Driven Animation. Dans cet article, je vais vous prĂ©senter en quelques mots comment ça fonctionne. Cela dit, mon but principal est de me concentrer sur les raisons pour lesquelles je pense que ce nâest pas la technique la plus adaptĂ©e qui est utilisĂ©e ici, et comment on pourrait faire diffĂ©remment. Enfin, en toute fin dâarticle je partagerai quelques ressources oĂč Scroll Driven Animation permet rĂ©ellement dâamĂ©liorer lâĂ©tat du web.
- Comment fonctionne une Scroll Driven Animation�
- Pourquoi nâest-ce pas trĂšs adaptĂ© sur cette dĂ©mo du stickyâŻ?
- Re-codons ce sticky header sans Scroll Driven Animation
- Conclusion
Comment fonctionne une Scroll Driven Animation�
Avant de parler de Scroll Driven Animation, parlons dâanimation tout court. En effet, si on revient Ă la source de celle ci, il sâagit dâune maniĂšre de passer dâun Ă©tat A, Ă un Ă©tat B, tout en restant fluide.

Traditionnellement, ce quâon va utiliser pour calculer la transition entre les deux Ă©tats est le temps (t) :
- Si t = 0, on est au tout dĂ©but de lâanimation, alors carrĂ© = rouge & angle = 0
- Si t = 1, on est Ă la fin de lâanimation, alors carrĂ© = vert & angle = 45°
Et au milieu de tout ça on va faire des interpolations pour calculer les étapes intermédiaires :
- Si t = 0.25, on a un peu avancĂ© dans lâanimation, mais pas beaucoup, donc carrĂ© = plutĂŽt rouge, et angle = ~11°
- Si t = 0.5, on est au milieu de lâanimation, donc carrĂ© = moite-moite entre rouge et vert, et angle = 22.5°
- Si t = 0.75, on plutÎt vers la fin, donc carré = plutÎt vert, et angle = ~34°
Et on peut faire ce calcul pour chacune des images de notre animation.
Cependant, une animation nâest pas forcĂ©ment dans le temps. Par exemple sur le schĂ©ma ci dessus, jâai fait une animation et finalement sa progression câest de gauche (t = 0) Ă droite (t = 1).
Dans le cas dâune Scroll Driven Animation, quâest-ce qui va ĂȘtre utilisĂ© pour tâŻ? Ce sera la quantitĂ© de scroll qui a Ă©tĂ© effectuĂ©e :
- si on est en haut de la page, alors on sera Ă lâĂ©tat A
- si on est en bas de la page, alors on sera Ă lâĂ©tat B
- et si on est Ă la moitiĂ© de la page, alors on sera Ă la moitiĂ© de lâanimation
En pratique, avec du code, ça veut dire que vous aurez besoin de définir plusieurs choses en CSS :
-
Une animation : qui traditionnellement est représentée avec des
@keyframes
en CSS@keyframes rotate-and-color { /* t = 0 si on reprend mon exemple ci-dessus */ 0% { background: red; transform: rotate(0deg); } /* t = 1 */ 100% { background: green; transform: rotate(45deg); } }
-
Les rĂšgles
animation
,animation-timeline
etanimation-range
sur lâĂ©lĂ©ment que vous voulez animer.square { animation: rotate-and-color linear; /* Ce n'est pas le temps, mais le scroll qui dicte la progression de l'animation */ animation-timeline: scroll(); /* Sur quelle distance de scroll doit se faire l'animation */ animation-range: normal 150px; }
Ici je définis:
- quelle animation doit ĂȘtre jouĂ©e via
animation
- que je dois utiliser le scroll et non le temps comme référentiel via
animation-timeline
(par dĂ©faut ce sera la premiĂšre scrollbar parente qui sera utilisĂ©e - si vous nâen avez pas dâimbriquĂ©es ce sera donc la scroll bar principale) - que je ne vais animer que sur les 150 premiers pixels verticaux via
animation-range
- quelle animation doit ĂȘtre jouĂ©e via
Ce qui nous donne ceci : (â ïž Chrome only Ă ce jour)
Quelques liens pour aller plus loin :
- Animate elements on scroll with Scroll-driven animations
- MDNÂ : animation-timeline: scroll()
- MDNÂ : animation-range
Pourquoi nâest-ce pas trĂšs adaptĂ© sur cette dĂ©mo du stickyâŻ?
Nous avons donc vu que lâanimation nâĂ©tait plus basĂ©e sur le temps mais sur le nombre de pixels qui ont Ă©tĂ© scrollĂ©s. Ainsi, si on pousse les tests un peu plus loin ça veut dire que :
-
Si je mâarrĂȘte en milieu de scroll, le site peut paraĂźtre cassĂ©. En tant que dev je comprends ce quâil se passe (et si jâai bien Ă©crit mon article, vous aussi :p) donc ça ne me choque pas outre mesure. Mais quâen pensera une personne non devâŻ? Je ne sais pas, mais je prĂ©sume que ça ne laissera pas la meilleure impression.
-
Si je scroll suffisamment vite pour rapidement dépasser mon
animation-range
, lâanimation nâapparaĂźt quasiment pas. On passe du dĂ©but Ă la fin trop rapidement pour voir lâanimation se dĂ©rouler. -
Si vous allez sur le CodePen de Jhey, vous verrez quâil y a besoin de calculer beaucoup de choses : chaque Ă©lĂ©ment animĂ© (lâavatar, la banniĂšre de couverture, le texte, etc.) a sa propre
animation-range
pour que lâeffet soit le plus chouette possible. Heureusement, Ă©tant donnĂ© que les Custom Properties (ou CSS Variables) sont disponibles dans les navigateurs, ce nâest pas une collection de Magic Numbers. Mais ça demande quand mĂȘme beaucoup de concentration Ă mon petit cerveau et je ne suis pas sĂ»r dâĂȘtre capable de relire ce genre de code 6 mois plus tard.
Nous allons donc voir ensemble comment jâai codĂ© le mĂȘme concept mais en utilisant des techniques diffĂ©rentes.
Re-codons ce sticky header sans Scroll Driven Animation
Ce nâest donc pas lâoutil idĂ©al pour mettre en place cette animation. Pourtant, on a trĂšs envie dâavoir le mĂȘme effet. Parce que câest stylĂ©. Donc comment peut-on faireâŻ?
Pour cela, codons ensemble cette dĂ©mo. Vous gagnez en prime cette magnifique grimace quand ma tĂȘte passe en sticky.
đĄ Avant de commencer, je tiens Ă vous signaler que pour que lâarticle reste le plus digeste possible, je vais simplifier certains Ă©lĂ©ments. Jâessayerai de mettre des laĂŻus sur les parties difficiles qui me paraissent importantes Ă Ă©clairer, mais il sera difficile dâĂȘtre exhaustif. Donc si vous voulez une solution complĂšte, nâhĂ©sitez pas Ă vous rĂ©fĂ©rer directement au code source de la dĂ©mo ou Ă tout simplement me contacter sur les rĂ©seaux sociaux si vous avez une question en particulier.
1. Mise en place de lâaffichage initial et Ă©tat final
Comme dâhabitude quand on fait du web, avant de parler style, animation ou quoique ce soit, commençons par nous mettre dâaccord sur la structure HTML que nous allons utiliser. Il y a notamment 4 parties qui seront importantes :
- la banniĂšre de couverture
- lâavatar
- le titre (et sous-titre)
- le bouton de contact
<header class="main-header">
<img class="cover" src="./cover.jpg" alt="Une magnifique étendue d'herbe" />
<div class="author">
<img
src="./cover.jpg"
alt="Julien Pradet, son casque sur les oreilles (parce qu'il a oublié de l'enlever pour la photo), sourire aux lÚvres et pas vraiment sérieux."
class="author__avatar"
/>
<div class="author__title">
<h1 class="author__name">Enchanté, Julien Pradet</h1>
<p class="author__description">Un dev' top moumoute</p>
</div>
<a class="author__contact" href="https://www.julienpradet.fr/">Me contacter</a>
</div>
</header>
Sur cette base nous allons maintenant pouvoir commencer Ă appliquer des styles. Il est encore trop tĂŽt pour parler animation, mais un peu plus haut nous avons vu quâil fallait penser Ă 2 Ă©tats : lâĂ©tat initial et lâĂ©tat final. Dans notre cas, nous allons donc devoir Ă©crire le CSS :
- quand le header nâest pas sticky et quâon peut se contenter dâafficher lâavatar, puis en dessous, le titre de la page
- quand le header est sticky et donc que lâavatar rĂ©trĂ©cit et le titre de la page passe sur la droite
Généralement, dans ce genre de situations, je sors mon outil de layout préféré : CSS Grid.

Ainsi, nous pouvons garder exactement la mĂȘme structure HTML pour des affichages rĂ©ellement diffĂ©rents. Le tout est de jouer avec les lignes et les colonnes pour aboutir Ă lâalignement rĂȘvĂ©. Dâailleurs, si jamais vous voulez aller plus loin, la semaine derniĂšre je vous parlais de Comment display: contents
peut vous aider Ă conserver la mĂȘme structure HTML avec CSS Grid.
Voici par exemple Ă quoi pourrait ressembler le CSS pour lâĂ©tat initial:
.author {
display: grid;
grid-template-areas:
'Avatar . .'
'Title Title Contact';
grid-template-columns: 8rem 1fr auto;
gap: 2rem;
}
.author__avatar {
grid-area: Avatar;
}
.author__title {
grid-area: Title;
}
.author__contact {
grid-area: Contact;
}
Ainsi, la seule chose quâon a besoin de changer pour lâĂ©tat final serait :
- dâajouter une classe
.author--sticky
Ă ladiv.author
- ajouter ces quelques lignes de CSS pour prĂ©ciser que lâorganisation est diffĂ©rente en mode sticky
.author--sticky { grid-template-areas: 'Avatar Title Contact'; grid-template-columns: 3rem 1fr auto; gap: 1rem; }
Comme vous pouvez le constater, pour lâinstant, pas de calculs particuliers. On code uniquement notre CSS afin quâil soit capable dâafficher indĂ©pendamment lâĂ©tat normal ou lâĂ©tat sticky. La seule contrainte est de minimiser la modification du HTML en ne sâautorisant quâĂ changer des classes (en lâoccurrence .author--stick
).
đĄ Quelques petites subtilitĂ©s que je nâai pas mentionnĂ© qui viendront plus tard:
- pas de
position: sticky
ni deposition: fixed
âŻ?- Ă quel moment lâavatar dĂ©passe-t-il sur la banniĂšre de couvertureâŻ?
đĄ Une autre question que vous pouvez vous poser : Comment gĂ©rer une max-width tout en sâassurant que le contenu est centrĂ©âŻ?
En effet, on ne peut pas rĂ©ellement se contenter dâun
max-width: 60rem; margin: 0 auto;
parce quâon veut que le fond dĂ©passe Ă gauche et Ă droite. Il a plusieurs solutions Ă cela, mais celle que jâai tendance Ă privilĂ©gier en ce moment est dâajouter des pseudo Ă©lĂ©ments Ă la grille qui seront responsables de gĂ©rer lâespace Ă gauche et Ă droite. Sur le principe ça ressemble Ă quelque chose de ce style:.author { --container-padding: 0.5rem; --max-container-width: 60rem; --gutter-width: max( var(--container-padding), calc((100% - var(--max-container-width)) / 2 + var(--container-padding)) ); grid-template-areas: 'GutterLeft Avatar . . GutterRight' 'GutterLeft Title Title Contact GutterRight'; grid-template-columns: var(--gutter-width) 8rem 1fr auto var(--gutter-width); } .author::before { content: ''; grid-area: GutterLeft; } .author::after { content: ''; grid-area: GutterLeft; }
Je passe un peu rapidement sur cette partie. Si ce nâest pas suffisamment clair, nâhĂ©sitez pas Ă mâenvoyer un message. Jâen ferai un article.
En tout cas, dans lâidĂ©e, ça nous a amenĂ© Ă ce rĂ©sultat :

2. Mise en place du JavaScript pour passer dâun Ă©tat Ă lâautre
Passons maintenant au code JavaScript qui sera responsable de passer dâun Ă©tat Ă lâautre. Le premier rĂ©flexe vu quâon parle de changement qui doit apparaĂźtre au scroll est dâajouter un listener sur lâĂ©vĂ©nement scroll
. Cependant cette mĂ©thode nâest pas pratique Ă utiliser et peut rapidement aboutir Ă des problĂšmes de performance. Essayez donc de lâĂ©viter au maximum.
Nous allons donc passer par un IntersectionObserver. Câest une API disponible dans les navigateurs qui permet de dĂ©clencher un Ă©vĂ©nement quand la visibilitĂ© dâun Ă©lĂ©ment Ă changer.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(entry.isIntersecting);
// Affiche true si l'élément observé est visible
// Affiche false sinon
});
});
observer.observe(element);
Dans notre cas, quel serait lâĂ©lĂ©ment quâon voudrait observerâŻ? Quand est-ce que la visibilitĂ© (ou lâinvisibilitĂ©) dâun Ă©lĂ©ment peut nous indiquer quâon veut dĂ©clencher le mode stickyâŻ? GĂ©nĂ©ralement il sâagit de lâĂ©lĂ©ment quâon va manipuler. Mais ici le principe est justement que notre .author
ne doit jamais disparaĂźtre. Nous allons donc plutĂŽt observer lâimage de la cover.
De plus, nous allons devoir configurer lâIntersectionObserver
pour quâil se dĂ©clenche non pas quand le dernier pixel de la cover nâest plus visible, mais un peu plus tĂŽt pour que lâavatar soit encore majoritairement visible avant de passer en mode sticky. On va donc essayer de viser la ligne rouge ci-dessous qui vient couper lâavatar vers 1/4 de sa hauteur.

Pour ce faire, nous allons passer des options supplĂ©mentaires Ă lâIntersectionObserver
et plus précisément, nous allons devoir configurer la rootMargin
 : câest elle qui nous permettra de dire Ă quelle distance de la fin de lâimage on doit dĂ©clencher lâobserver. Nous allons donc lui mettre quelques pixels nĂ©gatifs.
const author = document.querySelector('.author');
const cover = document.querySelector('.cover');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
// Si entry.isIntersecting, ça veut dire qu'une grande partie
// de la cover est visible => on enlĂšve la classe `author--sticky`
// Si au contraire !entry.isIntersecting, on a dépassé la ligne
// rouge => on ajoute la classe `author--sticky`
author.classList.toggle('author--sticky', !entry.isIntersecting);
});
},
{
// DĂšs qu'il y a ne serait-ce qu'un pixel au dessus de la ligne rouge
// => donc threshold = 1
threshold: 0,
// On met la hauteur en pixel entre la fin de l'image et la ligne rouge
// C'est négatif parce qu'on entre à l'intérieur de l'image. Si le nombre
// était positif, alors la ligne rouge se retrouverait sous l'image
rootMargin: '-32px 0px'
}
);
observer.observe(cover);
Et lĂ , magie, nous avons le passage du mode normal au mode sticky.
Pas tout Ă fait cependant parce que mĂȘme si lâorganisation de lâaffichage a changĂ©, ce nâest pas âstickyâ au sens oĂč si on scroll en bas de la page, le header nâest plus visible.
3. Faire fonctionner le sticky
Dans la plupart des cas sticky
vous allez pouvoir vous contenter dâutiliser position: sticky
.
Cependant il y a plusieurs problĂšmes qui font que ce nâest pas possible dans notre cas:
-
position: sticky
est relatif Ă la balise parente.Ătant donnĂ© que notre
div.author
est Ă lâintĂ©rieur dâune baliseheader
, alors sa zone de stickiness sâarrĂȘtera dĂšs quâon aura scrollĂ© au delĂ du header. Ce nâest pas un problĂšme totalement incontournable parce que nous pourrions travailler le HTML pour faire en sorte que notrediv.author
soit un enfant direct dubody
. Mais ça demanderait quelques ajustements sémantiques. -
Le navigateur nâarrivera pas Ă gĂ©rer un changement de hauteur en fonction de si on est
sticky
ou non.Dans le cadre de ma dĂ©mo, il nây a pas que la grille qui change. Afin de prendre le minimum de place, je change aussi le padding, la font-size et je cache le lien vers les rĂ©seaux sociaux. La consĂ©quence câest que la
height
de madiv.author
va changer selon si elle est enauthor--sticky
ou non. Et ça, le navigateur ne va pas du tout apprĂ©cier. Du tout. En gros, Ă peine vous aurez atteint le mode sticky, que le navigateur va vous en sortir instantanĂ©ment parce que la hauteur totale de votre page aura changĂ© et donc vous fera ressortir de lâIntersectionObserver. Vous pouvez vous rĂ©fĂ©rer Ă ce CodePen pour constater le problĂšme.
Pour ces raisons, nous nâallons pas utiliser position: sticky
, mais position: fixed
.
.author--sticky {
position: fixed;
top: 0;
left: 0;
right: 0;
}
Ainsi, quand on passe en mode sticky, la div.author
viendra effectivement coller le haut de lâĂ©cran. Mais si vous essayez cette solution vous constaterez que la hauteur totale du scroll de la page change au moment de lâajout de author--sticky
. Cela vient du fait que la hauteur de div.author
passe en fixed
et donc nâest plus comptabilisĂ©e dans le flow du document.
Lâastuce que jâai appliquĂ©e pour rĂ©gler ce problĂšme est de dĂ©finir une min-height
sur .main-header
. Et pour Ă©viter des tailles en dur, câest au niveau du JS que je vais gĂ©rer cela :
const mainHeader = document.querySelector('.main-header');
mainHeader.style.minHeight = `${mainHeader.clientHeight}px`;
// ... puis le reste du code JS qu'on avait déjà écrit
// avec l'IntersectionObserver & co
Ainsi, au moment de lâinitialisation de la page, je prĂ©cise que le header devra toujours faire au moins sa taille courante. De fait, quand div.author
passera en position: fixed
, .main-header
conservera la bonne taille.
đĄ Quand on fait des calculs de taille au chargement de la page, il faut penser Ă gĂ©rer des redimensionnement de navigateur. En effet, un cas courant est le fait de tourner votre tĂ©lĂ©phone pour mieux voir le contenu. Or, en position portrait et en position paysage, la hauteur du
main-header
ne sera pas du tout la mĂȘme. Souvent cette partie est assez complexe et ce nâest pas le cĆur du sujet que je veux aborder dans ce tutoriel. Si toutefois vous voulez voir comment jâai gĂ©rĂ© ça dans la dĂ©mo, je vous invite Ă jeter un coup dâĆil au code source.
4. Animons la transition vers le mode sticky
A ce stade, nous avons donc un header qui peut ĂȘtre en mode normal ou en mode sticky en fonction du niveau de scroll. Mais ce que je disais en introduction, câest que câest lâanimation qui amĂ©liore la perception de qualitĂ© de notre page. Donc voyons comment lâimplĂ©menter.
Pour cela, nous allons utiliser les animations FLIP. Pour celles et ceux qui me suivent depuis longtemps, ça vous parlera peut-ĂȘtre parce que jâai dĂ©jĂ Ă©crit un article Ă ce sujet il y a plus de 5 ans.
En quelques mots, le but de ces animations est de calculer la diffĂ©rence entre lâĂ©tat de dĂ©part et lâĂ©tat dâarrivĂ©e pour ensuite ĂȘtre capable de faire lâanimation uniquement en utilisant les propriĂ©tĂ©s CSS transform
et opacity
qui sont le seul moyen dâavoir des animations performantes sur le web.
Plus précisément cela se fait en 4 étapes, une par lettre :
- First : on enregistre la position de dĂ©part de lâĂ©lĂ©ment (ex :
{top: 100, left: 50}
) - Last : on place lâĂ©lĂ©ment dans sa position finale et on enregistre sa position (ex :
{top: 130, left: 50}
) - Invert : on calcule la différence entre les deux positions (ex :
{top: -30, left: 0}
) et on lâapplique Ă lâĂ©lĂ©ment. Ainsi, quand on applique cette transformation Ă lâĂ©lĂ©ment avec la propriĂ©tĂ© CSStransform
, on aura visuellement lâimpression quâil est Ă sa position initiale. - Play : puis on lance lâanimation. Cela peut se faire en dĂ©finissant la propriĂ©tĂ© CSS
transition: transform 0.4s ease-in-out
, puis en retirant la propriététransform
.
Ce paradigme est assez rĂ©pandu dans le domaine du web parce que câest la mĂ©thode magique qui permet de faire des animations performantes. Quelques outils qui peuvent vous aider Ă mettre ça en place:
- GSAP via FlipPlugin
- Svelte via
animate:flip
â€ïž - Vue via <TransitionGroup>
Dans le cadre de ce tutoriel, je suis en Vanilla alors jâai rĂ©utilisĂ© ce que jâavais dĂ©jĂ commencĂ© Ă Ă©crire dans mon prĂ©cĂ©dent tutoriel. Jâai cependant amĂ©liorĂ© la librairie pour que lâĂ©tape Play soit faite en CSS plutĂŽt que de la gĂ©rer manuellement en JS. Les animations sont donc encore plus performantes quâavant đ
Jâai par ailleurs ajoutĂ© un petit wrapper afin de simplifier son utilisation. De fait, on va pouvoir mettre en place lâanimation en 2 Ă©tapes :
-
Ajouter sur chacun des éléments que je veux animer une classe permettant de les identifier (ici
js-animate
)Â :- lâavatar
- le titre
- le contact
-
Dans le JS, entourer le changement de class de ma fonction
animate
+import { animate } from './animate.js'; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { + const elementsToAnimate = author.querySelectorAll('.js-animate'); + animate(elementsToAnimate, () => { author.classList.toggle('author--sticky', !entry.isIntersecting); + }); }); } /* ... */ );
đĄ Peut ĂȘtre remarquerez vous que lâAPI ressemble beaucoup Ă
document.startViewTransition
. Câest volontaire, mais je ne vais pas lâutiliser dans ce tutoriel par manque de support navigateur. Je ferai cela dit un article dans les prochaines semaines pour vous en parler en dĂ©tail. Jâai notamment des problĂ©matiques de performance Ă vous partager Ă ce sujet.
Si vous en ĂȘtes arrivĂ©s Ă ce stade, le plus gros du travail est fait.
Cela dit, il y a quelques difficultĂ©s que jâai passĂ© sous silence.
Le titre nâest pas toujours sur une seule ligne.
En effet sur certaines rĂ©solutions, le titre va ĂȘtre sur 2 lignes en mode normal et 1 seule en mode sticky.

Lâastuce est alors de transformer le HTML pour dĂ©couper chaque mot dans sa propre balise afin quâelle ait sa propre transition :
-<h1 class="author__name js-animate">
- Enchanté, Julien Pradet
+<h1 class="author__name">
+ <span class="js-animate">Enchanté,</span>
+ <span class="js-animate">Julien Pradet</span>
</h1>
đĄ A noter toutefois que ça ne marchera pas sur des Ă©lĂ©ments
inline
(le comportement par dĂ©faut dâunspan
). Il faudra donc penser à ajouter en CSS la propriétédisplay: inline-block
.Par ailleurs, si votre texte fait 5 paragraphes, ça va commencer à devenir assez lourd de calculer chaque animation FLIP. Donc à appliquer avec parcimonie.
Si je nâanime que les Ă©lĂ©ments internes, ça veut dire que le fond ne va pas sâanimer.
Comment faire pour que pendant lâanimation la zone couleur crĂšme soit elle aussi animĂ©eâŻ?
Lâastuce est de considĂ©rer le fond comme un enfant supplĂ©mentaire. Nous allons donc ajouter une nouvelle div vide Ă lâintĂ©rieur de div.author
:
<div class="author">
<div class="author__background js-animate"></div>
<!-- le reste du HTML de div.author -->
</div>
Il faut bien penser Ă lui ajouter la classe js-animate
vu quâelle fait partie des Ă©lĂ©ments Ă animer.
Par ailleurs, Ă©tant donnĂ© quâelle est vide, il va falloir trouver un moyen dâadapter sa taille au contenu. Pas question dâutiliser des hauteurs de pixel en dur. Une mĂ©thode qui marche en gĂ©nĂ©ral assez bien est plutĂŽt dâutiliser un position: absolute
 :
.author {
position: relative;
/* C'est en définissant le z-index ici que je m'assure
que les z-index des enfants n'impacteront pas l'extérieur */
z-index: 1;
/* Au passage, c'est grùce à ce margin-top négatif
que l'avatar va venir dépasser par dessus la cover */
margin-top: -4rem;
}
.author__background {
position: absolute;
inset: 0;
top: 4rem;
background: var(--color-creme);
z-index: -1;
}
.author--sticky {
position: fixed;
/* Il faut bien penser Ă remettre Ă 0 le margin-top
en sticky vu que l'avatar ne déborde plus */
margin-top: 0;
}
.author--sticky .author__background {
top: 0;
/* Et vous pouvez en profiter pour changer d'autres choses */
opacity: 0.5;
}
Enfin, il est important de ne jamais animer si prefers-reduced-motion
est activé.
En effet, certains contenus animĂ©s peuvent donner des nausĂ©es aux personnes qui les regardent - mĂȘme si lâanimation vous paraĂźt subtile.
Il existe une media query pour ça en CSS : @media (prefers-reduced-motion: reduce)
. En JS, on peut donc la reproduire via window.matchMedia('(prefers-reduced-motion: reduce)').matches
. Si vous ĂȘtes passĂ© directement par ma librairie animate
mentionnĂ©e plus haut, sachez que câest dĂ©jĂ gĂ©rĂ© Ă lâintĂ©rieur. Mais si vous utilisez un autre systĂšme, pensez Ă bien vĂ©rifier quâen activant lâoption, vos animations ne sont plus dĂ©clenchĂ©es.
Pour le tester dans les DevTools de Chrome, vous pouvez aller dans âź > More tools > Rendering, puis activer lâoption prefers-reduced-motion: reduce
:

5. Animons lâavatar
Enfin, cerise sur le gateau, ce qui rend la dĂ©mo de Jhey aussi chouette est le fait que son avatar sâanime quand il passe en sticky. Au dĂ©but il regarde vers la droite, et une fois passĂ© en sticky, il regarde vers le bas.
La technique utilisĂ©e pour ceci est dâutiliser un sprite. Le principe est dâavoir une seule image qui contient toutes les Ă©tapes de votre animation.

On lâoccurrence jâai pris une vidĂ©o de moi qui fait lâandouille, puis jâai rĂ©cupĂ©rĂ© les images intermĂ©diaires via ce site. Puis en CLI, jâai pu crĂ©er le montage ci-dessus:
montage images/*.jpg -mode concatenate -tile x1 sprite.jpg
Ensuite, en CSS lâastuce va ĂȘtre de jouer avec object-position
. En effet, quand on affiche une image par dĂ©faut, elle va sâafficher en suivant le ratio de celle-ci. Cependant, notre avatar, on veut quâil soit carrĂ©. Donc on va forcer en CSS une width
et une height
définie et ajouter object-position: cover
pour que toute la hauteur de lâimage soit affichĂ©e.
.author__avatar {
width: 8rem;
height: 8rem;
object-fit: cover;
}

Mais comme vous pouvez le constater, on se retrouve au milieu de deux images. Câest donc lâobject-position
va nous permettre de dire : est-ce quâon veut afficher le dĂ©but ou la finâŻ? En le mettant Ă 0, on va donc afficher le dĂ©but.
.author__avatar {
width: 8rem;
height: 8rem;
object-fit: cover;
+ object-position: 0 0;
}

Il ne reste plus quâĂ crĂ©er une animation en utilisant @keyframes
en CSS. Lâastuce ici va ĂȘtre dâutiliser steps(19)
comme timing-function
. En effet, mĂȘme si on est plutĂŽt habituĂ© Ă lâutilisation de linear
, ease-in-out
voire des cubic-bezier()
, il est aussi possible de dĂ©finir un nombre fini de frames dans votre transition. Câest donc ce steps(19)
qui va nous permettre de faire en sorte que ce soit toujours une tĂȘte entiĂšre qui soit visible. NâhĂ©sitez pas Ă faire varier le nombre en fonction du nombre dâimages prĂ©sentes dans votre sprite.
.author__avatar {
animation: 0.4s steps(19) 0s 1 normal forwards look-down;
}
@keyframes look-down {
from {
object-position: 0 0;
}
to {
object-position: 100% 0;
}
}
Vous pouvez tester sur lâimage ci-dessous en cliquant dessus:

Clique sur mon nez !
đĄ Pourquoi ne pas avoir utilisĂ©
transition: object-position 0.4s steps(19)
plutĂŽt quâanimation
âŻ?En effet, il est tout Ă fait possible dâutiliser la
transition-timing-function: steps(x)
pour une transition. Le souci est que si votre transition est interrompue en milieu dâanimation, alors elle continuera dâutiliser19
steps pour revenir Ă sa position initiale. Dans les faits, vous vous retrouverez donc avec une animation buggĂ©e. Cela nâarrive pas avec des@keyframes
et uneanimation
parce que cela dĂ©marrera toujours de lâĂ©tat initial vers lâĂ©tat final. Lâanimation ne pourra pas commencer dans un Ă©tat intermĂ©diaire.
Nous sommes maintenant capable dâanimer notre avatar. Comment lâassocier Ă notre animation du stickyâŻ? Ătant donnĂ© que lâanimation doit ĂȘtre jouĂ©e quâune seule fois Ă un moment prĂ©cis (quand le sticky se met en place, ou quand on lâenlĂšve), on va le faire en deux temps :
- Ajouter une classe CSS
.author--animating
qui, quand elle est prĂ©sente, dĂ©clenche lâanimation :
.author__avatar {
/* On affiche la premiĂšre frame */
object-position: 0 0;
}
.author--sticky .author__avatar {
/* En sticky, c'est plutĂŽt la derniĂšre frame */
object-position: 0 0;
}
/* On ajoute la media query pour les questions d'accessibilité
évoquées plus haut */
@media (prefers-reduced-motion: no-preference) {
/* Si pas author--sticky, alors on est en train de
revenir au mode normal donc on `reverse` l'animation */
.author--animating .author__avatar {
animation: 0.4s steps(19) 0s 1 reverse forwards look-down;
}
/* Si au contraire, on est en train de passer
en mode sticky, l'animation doit se dérouler dans
le sens normal */
.author--sticky.author--animating .author__avatar {
animation-direction: normal;
}
}
- Et dans le JS, dans lâIntersectionObserver, on ajoute cette classe
author--animating
 :
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
const elementsToAnimate = author.querySelectorAll('.js-animate');
animate(elementsToAnimate, () => {
+ author.classList.add('author--animating');
author.classList.toggle('author--sticky', !entry.isIntersecting);
+ }).then(() => {
+ // Une fois que l'animation est terminée on retire la class
+ // pour ĂȘtre sĂ»r que l'animation se rejouera au prochain coup
+ author.classList.remove('author--animating');
});
});
}
/* ... */
);
Et voilĂ đ Notre avatar est animĂ©âŻ!
Lazyloader le Sprite pour garder un premier affichage rapide
Jâai une derniĂšre remarque cela dit : nĂ©cessairement le sprite qui contient toutes les frames de lâanimation de ma tĂȘte est assez lourd. Le problĂšme de cette technique est donc que ça va ralentir lâaffichage initial de la page par rapport Ă un avatar normal. On parle dâun ratio x15 sur le poids de lâimage đ± Histoire de garder des performances convenables, jâai donc optĂ© pour une sorte de lazyloading :
<img
src="./avatar.jpg"
data-sprite-src="./sprite_avatar.jpg"
width="232"
height="232"
class="author__avatar js-animate js-avatar"
/>
Initialement, le navigateur va charger mon avatar simple qui contient uniquement la premiĂšre frame de ma tĂȘte. Puis, en JavaScript je vais venir modifier lâURL de lâimage pour ĂȘtre en mesure de faire lâanimation. Ainsi, la grosse image est chargĂ©e plus tard et elle nâest chargĂ©e que si le JavaScript a rĂ©ussi Ă sâinitialiser.
Pour appliquer cette mĂ©thode en JS jâai ajoutĂ© ce petit script:
const sprite = document.createElement('img');
sprite.src = avatar.dataset.spriteSrc;
sprite.addEventListener('load', () => {
avatar.src = sprite.src;
});
Lâastuce est de faire en sorte dâattendre que lâimage ait fini de charger avant de changer lâattribut src
de mon avatar. Si je nâavais pas fait ça, jâaurais pu avoir un flash blanc sur mon image.
Conclusion
Nous voilĂ arrivĂ© au bout. đ„ł Si vous ressentez le besoin de creuser un peu plus, nâoubliez pas que le code source de la dĂ©mo est disponible sur mon GitHub.
Est-ce que malgrĂ© tout on a bien fait notre travailâŻ? Pour ça revenons Ă la liste des problĂšmes quâon avait repĂ©rĂ© sur les Scroll Driven Animations :
- on ne reste plus bloqué dans un état intermédiaire pendant le scroll
- quelle que soit la vitesse de scroll, lâanimation est lisible : tout le monde verra le header sâanimer pour passer dâune position normale Ă sticky
- il nây a plus de calcul savant pour calculer des positions et dimensions. Tout est gĂ©rĂ© avec CSS Grid puis calculĂ© automatiquement en JS. La seule dimension manuelle est celle du
rootMargin
de lâIntersectionObserver
A priori, la mission est donc plutĂŽt remplie. Dâautant plus que cette technique permet aussi :
- de changer la taille du texte sans pour autant souffrir de problĂšmes de performance
- dâanimer des changements plus complexes (ex : le texte qui passe de 2 lignes Ă une seule en mobile)
Par contre, elle crĂ©e aussi de nouveaux problĂšmes. Notamment, si on scroll peu, on se retrouve avec un grand espace blanc. Et si vraiment on joue avec la limite, on peut mĂȘme se retrouver avec le sticky qui vient par dessus la cover.

Personnellement je pense que le compromis est acceptable comparĂ© Ă lâĂ©tat cassĂ© de la Scroll Driven Animation. Par contre câest ce qui me fait dire que si je devais intĂ©grer ce design sur un site en production, je continuerais Ă faire quelques adaptations. Notamment, jâessayerais de travailler pour que la diffĂ©rence de hauteur entre sticky et non sticky soit moins flagrante afin de diminuer la hauteur de lâespace blanc que cela crĂ©e.
En tout cas câĂ©tait une chouette dĂ©mo Ă faire et ça mâa remotivĂ© Ă faire des animations partout. Cela dit, je veux me pencher sur le sujet de lâAPI View Transitions parce quâen se jetant corps et Ăąmes dedans, jâai peur que ça ouvre des nouvelles problĂ©matiques de performance. Je publierai un article Ă ce sujet courant septembre je pense.
Si vous voulez continuer de suivre mes tutoriels, nâhĂ©sitez pas Ă me suivre sur les rĂ©seaux sociaux (Mastodon, LinkedIn ou Twitter, voire mĂȘme RSS). Jâessaye de publier un article par semaine autour de la performance web et du dĂ©veloppement front-end dans son ensemble pour partager mes dĂ©couvertes et ma façon de travailler.
Si lire des articles vous paraĂźt trop long et que vous avez des besoins plus urgents Ă adresser, sachez que je suis disponible en freelance pour vous aider Ă monter en compĂ©tence, rĂ©gler vos soucis de performance ou ajouter un petit plus Ă votre site web. Alors nâhĂ©sitez pas Ă me contacterâŻ!
Des bisous et Ă la semaine prochaine đ
Enfin, si cet article vous a ouvert lâappetit sur les Scroll Driven Animations, voici quelques ressources qui mĂ©ritent votre attention :
- Animate elements on scroll with Scroll-driven animations
- A bunch of demos and tools to show off Scroll-driven Animations
Ce sont ces ressources qui me font dire quâil y a bel et bien un usage pour cette nouvelle API. Cependant, je restreindrais son utilisation Ă certains cas :
- soit avec des zones de scroll trĂšs courtes
- soit avec du Scroll Snap
- soit avec des jeux dâopacitĂ©
- soit pour dâautres usages trĂšs diffĂ©rents (cf. Position-Driven Styles & Fit-to-Width Text qui rĂ©volutionneront votre perception des possibilitĂ©s quâouvrent les Scroll Driven Animations)
- soit avec animations plus subtiles pour que ça nâait jamais lâair cassĂ© ou temporaire Shrinking Header
Vous en voyez dâautresâŻ? đ€
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 :)