Transitions de vue React
Animez les changements d'état de l'interface en utilisant le natif document.startViewTransition du navigateur. Déclarez le quoi avec <ViewTransition>, déclenchez le quand avec startTransition / useDeferredValue / Suspense, contrôlez le comment avec les classes CSS. Les navigateurs non supportés ignorent gracieusement les animations.
Quand animer
Chaque <ViewTransition> doit communiquer une relation spatiale ou une continuité. Si vous ne pouvez pas articuler ce qu'elle communique, ne l'ajoutez pas.
Implémentez tous les motifs applicables de cette liste, dans cet ordre :
| Priorité | Motif | Ce qu'il communique |
|---|---|---|
| 1 | Élément partagé (name) |
« La même chose — en approfondissant » |
| 2 | Suspense reveal | « Les données ont été chargées » |
| 3 | Identité de liste (key par élément) |
« Les mêmes éléments, nouvel arrangement » |
| 4 | Changement d'état (enter/exit) |
« Quelque chose a disparu/réapparu » |
| 5 | Changement de route (au niveau du layout) | « Aller à un nouvel endroit » |
C'est un ordre d'implémentation, pas une liste « choisir un ». Implémentez chaque motif qui s'applique à l'app. Ne sautez un motif que s'il n'y a pas de cas d'usage pour lui.
Choisir le style d'animation
| Contexte | Animation | Pourquoi |
|---|---|---|
| Navigation hiérarchique (liste → détail) | nav-forward / nav-back typé par clé |
Communique la profondeur spatiale |
| Navigation latérale (onglet à onglet) | <ViewTransition> nu (fade) ou default="none" |
Pas de profondeur à communiquer |
| Suspense reveal | Props string enter/exit |
Contenu arrivant |
| Revalidation / rafraîchissement en arrière-plan | default="none" |
Silencieux — aucune animation nécessaire |
Réservez les slides directionnels pour la navigation hiérarchique (liste → détail) et les séquences ordonnées (photo précédente/suivante, carousel, résultats paginés). Pour les séquences ordonnées, la direction communique la position : « suivant » glisse de droite, « précédent » de gauche. La navigation latérale/non-ordonnée (onglet à onglet) ne doit pas utiliser de slides directionnels — cela implique faussement une profondeur spatiale.
Disponibilité
- Next.js : Ne pas installer
react@canary— l'App Router regroupe déjà React canary en interne.ViewTransitionfonctionne prêt à l'emploi.npm ls reactpeut afficher une version ressemblant à stable ; c'est attendu. - Sans Next.js : Installez
react@canary react-dom@canary(ViewTransitionn'est pas dans React stable). - Support navigateur : Chromium 111+, Firefox 144+, Safari 18.2+. Dégradation gracieuse sur les navigateurs non supportés.
Flux de travail d'implémentation
Quand vous ajoutez des transitions de vue à une app existante, suivez references/implementation.md étape par étape. Commencez par l'audit — ne le sautez pas. Copiez les recettes CSS de references/css-recipes.md dans la feuille de style globale — n'écrivez pas votre propre CSS d'animation.
Concepts fondamentaux
Le composant <ViewTransition>
import { ViewTransition } from 'react';
<ViewTransition>
<Component />
</ViewTransition>
React attribue automatiquement un view-transition-name unique et appelle document.startViewTransition en arrière-plan. N'appelez jamais startViewTransition vous-même.
Déclencheurs d'animation
| Déclencheur | Quand il se déclenche |
|---|---|
| enter | <ViewTransition> inséré pour la première fois pendant une Transition |
| exit | <ViewTransition> supprimé pour la première fois pendant une Transition |
| update | Mutations DOM à l'intérieur d'une <ViewTransition>. Avec VT imbriquées, la mutation s'applique à la plus interne |
| share | VT nommée se démonte et une autre avec le même name se monte dans la même Transition |
Seuls startTransition, useDeferredValue ou Suspense activent les VT. Un setState régulier n'anime pas.
Règle de placement critique
<ViewTransition> active enter/exit seulement si elle apparaît avant tous les nœuds DOM :
// Fonctionne
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
// Cassé — div enveloppe la VT, supprimant enter/exit
<div>
<ViewTransition enter="auto" exit="auto">
<div>Content</div>
</ViewTransition>
</div>
Styliser avec les classes de transition de vue
Props
Valeurs : "auto" (cross-fade du navigateur), "none" (désactivé), "class-name" (CSS personnalisé), ou { [type]: value } pour les animations type-spécifiques.
<ViewTransition default="none" enter="slide-in" exit="slide-out" share="morph" />
Si default est "none", tous les déclencheurs sont désactivés sauf s'ils sont explicitement listés.
Pseudo-éléments CSS
::view-transition-old(.class)— snapshot sortant::view-transition-new(.class)— snapshot entrant::view-transition-group(.class)— conteneur::view-transition-image-pair(.class)— paire old + new
Voir references/css-recipes.md pour les recettes d'animation prêtes à l'emploi.
Types de transition
Étiquetez les transitions avec addTransitionType pour que les VT puissent choisir différentes animations en fonction du contexte. Appelez-la plusieurs fois pour empiler les types — différentes VT dans l'arbre réagissent à différents types :
startTransition(() => {
addTransitionType('nav-forward');
addTransitionType('select-item');
router.push('/detail/1');
});
Passez un objet pour mapper les types aux classes CSS. Fonctionne sur enter, exit, et share :
<ViewTransition
enter={{ 'nav-forward': 'slide-from-right', 'nav-back': 'slide-from-left', default: 'none' }}
exit={{ 'nav-forward': 'slide-to-left', 'nav-back': 'slide-to-right', default: 'none' }}
share={{ 'nav-forward': 'morph-forward', 'nav-back': 'morph-back', default: 'morph' }}
default="none"
>
<Page />
</ViewTransition>
enter et exit ne doivent pas être symétriques. Par exemple, fade in mais slide out directionnellement :
<ViewTransition
enter={{ 'nav-forward': 'fade-in', 'nav-back': 'fade-in', default: 'none' }}
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
default="none"
>
TypeScript : ViewTransitionClassPerType nécessite une clé default dans l'objet.
Pour les apps avec plusieurs pages, extrayez la VT typée par clé dans un wrapper réutilisable :
export function DirectionalTransition({ children }: { children: React.ReactNode }) {
return (
<ViewTransition
enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
default="none"
>
{children}
</ViewTransition>
);
}
router.back() et bouton de retour du navigateur
router.back() et les boutons de retour/avant du navigateur ne pas déclencher les transitions de vue (popstate est synchrone, incompatible avec startViewTransition). Utilisez router.push() avec une URL explicite à la place.
Types et Suspense
Les types sont disponibles pendant la navigation mais pas lors des révélations Suspense ultérieures (transitions séparées, pas de type). Utilisez les cartes de type pour enter/exit au niveau page ; utilisez des props string simples pour les révélations Suspense.
Transitions d'élément partagé
Le même name sur deux VT — l'une se démontant, l'autre se montant — crée une morphe d'élément partagé :
<ViewTransition name="hero-image">
<img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
</ViewTransition>
// Sur l'autre vue — même name
<ViewTransition name="hero-image">
<img src="/full.jpg" />
</ViewTransition>
- Une seule VT avec un
namedonné peut être montée à la fois — utilisez des noms uniques (photo-${id}). Attention aux composants réutilisables : si un composant avec une VT nommée est rendu à la fois dans une modal/popover et une page, les deux se montent simultanément et cassent la morphe. Soit rendez le name conditionnel (via une prop), soit déplacez la VT nommée hors du composant partagé vers le consommateur spécifique. shareprend précédence surenter/exit. Réfléchissez à chaque chemin de navigation : quand aucune paire correspondante ne se forme (par ex., la page cible n'a pas le même name),enter/exitse déclenche à la place. Considérez si l'élément a besoin d'une animation de secours pour ces chemins.- N'utilisez jamais une exit fade-out sur les pages avec morphes partagées — utilisez un slide directionnel à la place.
Motifs courants
Enter/Exit
{show && (
<ViewTransition enter="fade-in" exit="fade-out"><Panel /></ViewTransition>
)}
Réordonner une liste
{items.map(item => (
<ViewTransition key={item.id}><ItemCard item={item} /></ViewTransition>
))}
Déclenchez à l'intérieur de startTransition. Évitez les <div> wrapper entre la liste et la VT.
Composer les éléments partagés avec l'identité de liste
Les éléments partagés et l'identité de liste sont des préoccupations indépendantes — ne confondez pas l'une avec l'autre. Quand un élément de liste contient un élément partagé (par ex., une image qui morphe en vue détail), utilisez deux limites <ViewTransition> imbriquées :
{items.map(item => (
<ViewTransition key={item.id}> {/* identité de liste */}
<Link href={`/items/${item.id}`}>
<ViewTransition name={`item-image-${item.id}`} share="morph"> {/* élément partagé */}
<Image src={item.image} />
</ViewTransition>
<p>{item.name}</p>
</Link>
</ViewTransition>
))}
La VT extérieure gère les réordonnancements de liste/animations enter. La VT intérieure gère la morphe d'élément partagé inter-routes. Manquer l'une ou l'autre couche signifie que l'animation ne se produira pas silencieusement.
Forcer la re-entrée avec key
<ViewTransition key={searchParams.toString()} enter="slide-up" default="none">
<ResultsGrid />
</ViewTransition>
Attention : Si vous enveloppez <Suspense>, changer key remonte la limite et refait la requête.
Suspense Fallback to Content
Cross-fade simple :
<ViewTransition>
<Suspense fallback={<Skeleton />}><Content /></Suspense>
</ViewTransition>
Révélation directionnelle :
<Suspense fallback={<ViewTransition exit="slide-down"><Skeleton /></ViewTransition>}>
<ViewTransition enter="slide-up" default="none"><Content /></ViewTransition>
</Suspense>
Pour plus de motifs, voir references/patterns.md.
Comment plusieurs VT interagissent
Chaque VT correspondant au déclencheur se déclenche simultanément dans un seul document.startViewTransition. Les VT dans différentes transitions (navigation vs résolution Suspense ultérieure) ne se font pas concurrence.
Utiliser default="none" généreusement
Sans lui, chaque VT se déclenche en cross-fade navigateur sur chaque transition — résolutions Suspense, mises à jour useDeferredValue, revalidations en arrière-plan. Utilisez toujours default="none" et activez explicitement seulement les déclencheurs désirés.
Deux motifs coexistent
Motif A — Slides directionnels : VT typée par clé sur chaque page, se déclenche pendant la navigation. Motif B — Suspense reveals : Props string simples, se déclenche quand les données se chargent (pas de type).
Ils coexistent parce qu'ils se déclenchent à différents moments. default="none" sur les deux empêche l'interférence croisée. Associez toujours enter avec exit. Placez les VT directionnelles dans les composants page, pas les layouts.
Limitation des VT imbriquées
Quand une VT parent se démonte, les VT imbriquées à l'intérieur ne se déclenchent pas leurs propres enter/exit — seule la VT la plus externe s'anime. Les animations staggerées par élément pendant la navigation page ne sont pas possibles aujourd'hui. Voir react#36135 pour un opt-in expérimental.
Intégration Next.js
Pour le setup Next.js (drapeau experimental.viewTransition, prop transitionTypes sur next/link, motifs App Router, Server Components), voir references/nextjs.md.
Accessibilité
Ajoutez toujours le CSS reduced motion de references/css-recipes.md à votre feuille de style globale.
Fichiers de référence
references/implementation.md— Flux de travail d'implémentation étape par étape.references/patterns.md— Motifs, timing d'animation, events API, dépannage.references/css-recipes.md— Recettes d'animation CSS prêtes à l'emploi.references/nextjs.md— Motifs App Router Next.js et détails des Server Components.
Document compilé complet
Pour le guide complet avec tous les fichiers de référence développés : AGENTS.md