Journal
Transitions de pages & Shared Element
Écran de chargement, rideau cobalt et transition Shared Element : les coulisses de la navigation fluide de mon portfolio entre Next.js App Router, GSAP et Lenis.
Sur le web traditionnel, le changement de page est presque toujours une rupture : on clique sur un lien, l'écran clignote ou se vide brièvement, puis le nouveau contenu apparaît d'un bloc. Pour ce portfolio, je voulais exactement l'inverse : une sensation d'application native où chaque transition prolonge le mouvement et guide le regard sans le moindre à-coup.
1. Le preloader initial : charger vite et sans frustration
Avant même de parler de transitions entre deux pages, il y a la toute première arrivée sur le site. Plutôt que de laisser l'utilisateur subir le pop-in disgracieux des polices ou les micro-saccades de compilation des shaders 3D au-dessus de la ligne de flottaison, j'ai mis en place un écran de chargement sur mesure.
Ce preloader ne tourne pas sur un faux timer arbitraire : il suit de véritables promesses système. Il attend que les fontes soient prêtes (document.fonts.ready), que les visuels critiques du haut de page soient chargés et, sur grand écran, que les modules WebGL du verre cannelé et du monogramme 3D soient instanciés.
Le tout est borné de façon stricte : un plancher de 700 ms pour laisser le temps à l'œil d'apprécier la composition, et un plafond absolu de 2200 ms pour ne jamais bloquer qui que ce soit si le réseau ralentit. Une fois joué, l'état est mémorisé en sessionStorage pour ne plus jamais se répéter durant la visite.

2. Transition standard : le rideau cobalt et la typographie variable
Pour la navigation principale (passer de l'Accueil à la page À propos ou au formulaire de Contact), le site déclenche une transition par rideau cobalt dynamique. Au clic, un rideau monte depuis le bas de l'écran avec une courbe expo.inOut très tranchée.
Le piège classique sur ce genre d'effet, c'est le texte intermédiaire. Je voulais afficher le nom de la page de destination qui passe d'une graisse ultra-fine (Archivo Thin, wght 150) à une graisse très lourde (Archivo Fat, wght 800). Problème : en gras, les mots prennent plus de place et peuvent sauter à la ligne au beau milieu de l'animation, ce qui ruine tout l'effet.
La parade technique a consisté à pré-calculer le texte dans son état le plus large dans un conteneur temporaire masqué, afin de verrouiller mathématiquement les sauts de ligne en amont. L'interpolation typographique se fait ensuite sans le moindre décalage spatial.
3. Le graal du « Shared Element » avec Next.js App Router
Quand on consulte la liste des projets ou les articles de ce journal, utiliser un simple rideau occultant aurait été dommage. L'expérience idéale, c'est celle des applications mobiles soignées : cliquer sur une carte et voir son image s'étirer naturellement pour devenir la bannière de l'article.
Dans Next.js App Router, c'est notoirement complexe à réaliser : dès que l'URL change, React démonte l'arborescence de la page de départ pour monter la nouvelle. Si l'on attend la nouvelle page, l'élément d'origine n'existe plus dans le DOM.
Pour contourner ce démontage, la mécanique fonctionne en 4 temps précis :
1. À l'interception du clic, on mesure les dimensions et la position exacte de la miniature à l'écran via getBoundingClientRect().
2. On clone immédiatement l'image dans un élément flottant indépendant, injecté directement à la racine de document.body avec son arrondi (border-radius) et ses coordonnées exactes.
3. GSAP prend le relais et anime ce clone pour l'étirer jusqu'aux dimensions complètes de la fenêtre (100vw × 100vh). Pendant que l'écran est masqué par ce visuel en plein vol, Next.js effectue son router.push() en tâche de fond.
4. Dès que la page d'arrivée est montée, un attribut data-shared-target signale l'emplacement de la couverture définitive. Le clone s'anime doucement pour venir s'aimanter (docking) dans cette boîte finale avant d'être discrètement détruit.
4. Synchronisation Lenis et accessibilité
Le smooth scrolling géré par Lenis apporte un confort exceptionnel, mais en pleine transition de page, il peut vite devenir votre pire ennemi : si l'inertie du scroll continue pendant qu'un clone est interpolé, les coordonnées sautent.
Nous avons donc interconnecté un système d'événements stricts : dès que la navigation démarre (wu:nav-start), Lenis est gelé. Dès que l'écran est masqué par le rideau ou le calque flottant (wu:nav-cover), le scroll est réinitialisé à zéro de manière instantanée et invisible. Enfin, une fois la transition achevée (wu:nav-end), Lenis recalcule la nouvelle hauteur de page et libère le défilement.
Et pour respecter tous les visiteurs, l'ensemble du système écoute en permanence prefers-reduced-motion : si l'utilisateur a désactivé les animations dans son système d'exploitation, les transitions complexes sont instantanément court-circuitées pour une navigation classique sans aucun effet superflu.