View Transitions dans Next.js : animations de pages sans bibliothèque
View Transitions dans Next.js : animations de pages sans bibliothèque
Dans le dernier article, j'ai montré que l'INP mesure l'écart entre un clic et le frame qui le reflète, et qu'une seule longue tâche sur le thread principal peut ruiner cet écart, peu importe le coût de l'animation elle-même. Les transitions de pages sont la version extrême de ce problème : elles demandent au thread principal de faire son travail le plus coûteux – rendre une page entièrement nouvelle – et de faire tourner une animation fluide à 60fps, simultanément.
Ce que les bibliothèques faisaient vraiment : FLIP
En 2015, Paul Lewis a décrit une technique qu'il a appelée FLIP – First, Last, Invert, Play. On ne peut pas animer à moindre coût un élément d'une position de layout à une autre, car top et left forcent un recalcul de layout à chaque frame. Alors on triche. On enregistre la position First, on laisse le DOM sauter instantanément à son état Last, on Inverse ce saut avec un transform qui fait que l'élément semble ne pas avoir bougé, et enfin on Play le transform jusqu'à zéro. L'utilisateur voit un glissement fluide. Le navigateur n'a jamais animé qu'un transform.
La View Transitions API reprend cette astuce et la donne au navigateur. Au lieu de garder deux arbres DOM vivants et de les mesurer, le navigateur photographie l'ancien état, laisse le DOM se mettre à jour librement, photographie le nouvel état, et anime entre les deux images – sur le compositeur.
La séquence et l'arbre qu'elle construit
Le cœur de l'API est un seul appel. Encapsulez une mise à jour DOM dans document.startViewTransition() et le navigateur exécute une séquence fixe :
javascript
function navigate(update) {if (!document.startViewTransition) {update(); // pas de support : juste mettre à jour, pas d'animationreturn;}document.startViewTransition(() => update());}
Cet arbre de pseudo-éléments vaut la peine d'être connu par son nom :
css
::view-transition /* la superposition couvrant toute la page */::view-transition-group(name) /* anime la taille et la position */::view-transition-image-pair(name)::view-transition-old(name) /* le snapshot "avant" */::view-transition-new(name) /* le snapshot "après" */
La division est importante. Le groupe est ce qui se déplace et change de taille – donc animation-duration y appartient. La paire old/new est ce qui fait le fondu croisé.
Ce qui surprend tout le monde : un snapshot est une photo, pas un élément
::view-transition-old n'est pas votre ancien élément rendu plus petit. C'est une image statique de lui, capturée une fois, à un instant.
Tout découle de là. Une <video> dans une transition gèle sur une frame pendant toute la durée. Un spinner arrête de tourner. Le texte dans le snapshot ne peut pas être sélectionné. Un GIF s'arrête. Une animation CSS dans l'élément capturé se met en pause à mi-chemin.
À 200ms, rien de tout ça n'a d'importance. À 1000ms, tout devient évident et légèrement cassé. C'est la vraie raison de garder les transitions courtes.
Nommage : tout le contrat
Par défaut, on obtient un fondu de la page entière. Le comportement intéressant commence quand on donne à un élément un view-transition-name. Le même nom des deux côtés signifie que le navigateur déplace et met à l'échelle l'un dans l'autre.
css
.card-image {view-transition-name: hero;}::view-transition-group(hero) {animation-duration: 400ms;animation-timing-function: ease-in-out;}
La règle absolue : un view-transition-name doit être unique dans le document à tout moment.
React 19.2 et Next.js 16 : un seul composant
React résout cela avec le composant <ViewTransition>, et Next.js l'utilise depuis la version 16 sans configuration.
tsx
import { ViewTransition } from "react";import Image from "next/image";import Link from "next/link";export function ProjectCard({ project }) {return (<Link href={"/projects/" + project.slug}><ViewTransition name={"project-" + project.slug}><Image src={project.cover} alt={project.title} /></ViewTransition></Link>);}
Un détail des docs Next.js mérite d'être répété car il échoue silencieusement : share et default="none" doivent voyager ensemble. Avec default="none" et sans prop share, la paire cesse silencieusement de se morpher.
Direction : dire à l'utilisateur où il est allé
Next.js 16.2 a ajouté transitionTypes à <Link>, ce qui permet à <ViewTransition> de mapper chaque tag sur une animation différente.
tsx
<Link href="/projects/runvalis" transitionTypes={["nav-forward"]}>Runvalis</Link><ViewTransitionenter={{ "nav-forward": "slide-in", "nav-back": "slide-out", default: "none" }}exit={{ "nav-forward": "slide-out", "nav-back": "slide-in", default: "none" }}default="none"><main>{children}</main></ViewTransition>
La version sans framework du tout
css
@view-transition {navigation: auto;}
C'est toute la configuration. C'est l'argument le plus fort que cette API n'est pas une fonctionnalité React qui utilise CSS par hasard, mais une fonctionnalité du navigateur que React expose joliment.
Pièges et bonnes pratiques
- Toujours le traiter comme une amélioration progressive. Là où le navigateur manque de support, rien ne casse.
- Déboguer au ralenti. Le panneau Animations de Chrome DevTools permet de réduire la lecture à 25% ou 10%.
- Respecter
prefers-reduced-motion. Les diapositives directionnelles simulent un mouvement sur tout le viewport. - La superposition mange les clics par défaut. Définir
::view-transition { pointer-events: none }. - Les snapshots coûtent de la mémoire.
- Ancrer l'en-tête.
- Une view transition est invisible pour les technologies d'assistance.
Conclusion
View Transitions sont le même argument appliqué à la navigation : au lieu d'exécuter FLIP à la main, on déclare ce qui connecte deux états et on laisse le navigateur dessiner la connexion à partir de deux photos.