Transitions d'affichage pour un même document dans les applications monopages

Lorsqu'une transition d'affichage s'exécute sur un seul document, elle est appelée transition de vue sur le même document. C'est généralement le cas dans les applications monopages (SPA) où JavaScript est utilisé pour mettre à jour le DOM. Les transitions d'affichage du même document sont compatibles avec Chrome à partir de Chrome 111.

Pour déclencher une transition vers le même document, appelez document.startViewTransition:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

Lorsqu'il est appelé, le navigateur capture automatiquement des instantanés de tous les éléments sur lesquels une propriété CSS view-transition-name est déclarée.

Il exécute ensuite le rappel transmis qui met à jour le DOM, après quoi il prend des instantanés du nouvel état.

Ces instantanés sont ensuite organisés dans une arborescence de pseudo-éléments et animés grâce à la puissance des animations CSS. Des paires d'instantanés de l'ancien et du nouvel état passent en douceur de leur ancienne position et taille à leur nouvel emplacement, tandis que leur contenu passe en fondu. Si vous le souhaitez, vous pouvez utiliser du code CSS pour personnaliser les animations.


La transition par défaut: le fondu enchaîné

La transition de la vue par défaut est un fondu enchaîné. Il s'agit donc d'une belle introduction à l'API:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

updateTheDOMSomehow fait passer le DOM au nouvel état. Vous pouvez le faire comme vous le souhaitez. Par exemple, vous pouvez ajouter ou supprimer des éléments, modifier les noms des classes ou modifier les styles.

Et voilà, les pages en fondu enchaîné:

Fondu enchaîné par défaut. Démonstration minimale : Source :

Un fondu enchaîné n'est pas si impressionnant. Heureusement, les transitions peuvent être personnalisées, mais vous devez d'abord comprendre le fonctionnement de ce fondu enchaîné standard.


Fonctionnement de ces transitions

Mettons à jour l'exemple de code précédent.

document.startViewTransition(() => updateTheDOMSomehow(data));

Lorsque .startViewTransition() est appelé, l'API capture l'état actuel de la page. Cela inclut la prise d'instantanés.

Une fois l'opération terminée, le rappel transmis à .startViewTransition() est appelé. C'est là que le DOM est modifié. Ensuite, l'API capture le nouvel état de la page.

Une fois le nouvel état capturé, l'API construit une arborescence de pseudo-élément comme celle-ci:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

Le ::view-transition apparaît en superposition, par-dessus tout le reste de la page. Cela peut être utile si vous souhaitez définir une couleur d'arrière-plan pour la transition.

::view-transition-old(root) est une capture d'écran de l'ancienne vue, tandis que ::view-transition-new(root) est une représentation en direct de la nouvelle vue. Les deux s'affichent en tant que "contenu de remplacement" CSS (comme <img>).

L'ancienne vue s'anime de opacity: 1 à opacity: 0, tandis que la nouvelle s'anime de opacity: 0 à opacity: 1 pour créer un fondu enchaîné.

Toutes les animations sont réalisées à l'aide d'animations CSS. Vous pouvez donc les personnaliser avec CSS.

Personnaliser la transition

Tous les pseudo-éléments de transition d'affichage peuvent être ciblés avec CSS. Puisque les animations sont définies à l'aide de CSS, vous pouvez les modifier à l'aide des propriétés d'animation CSS existantes. Exemple :

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

Avec ce seul changement, le fondu est maintenant très lent:

Fondu enchaîné long. Démonstration minimale : Source :

Ce n'est toujours pas impressionnant. À la place, le code suivant implémente la transition "Axe partagé" de Material Design:

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

Et voici le résultat:

Transition "Axe partagé". Démonstration minimale : Source :

Effectuer une transition avec plusieurs éléments

Dans la démonstration précédente, toute la page est impliquée dans la transition de l'axe partagé. Cela fonctionne pour la majeure partie de la page, mais cela ne semble pas tout à fait correct pour l'en-tête, car il glisse vers l'extérieur pour revenir à l'intérieur.

Pour éviter cela, vous pouvez extraire l'en-tête du reste de la page afin de l'animer séparément. Pour ce faire, attribuez un view-transition-name à l'élément.

.main-header {
  view-transition-name: main-header;
}

Vous pouvez définir la valeur de view-transition-name comme bon vous semble (sauf pour none, qui signifie qu'il n'y a pas de nom de transition). Il permet d'identifier de manière unique l'élément lors de la transition.

Et le résultat de cela:

Transition "Axe partagé" avec en-tête fixe. Démonstration minimale : Source :

Maintenant, l'en-tête reste en place et se fonde enchaîné.

Cette déclaration CSS a entraîné la modification de l'arborescence des pseudo-éléments:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

Il existe maintenant deux groupes de transition. une pour l'en-tête et une autre pour le reste. Celles-ci peuvent être ciblées indépendamment avec le CSS et disposer de différentes transitions. Toutefois, dans ce cas, main-header conserve la transition par défaut, qui est un fondu enchaîné.

La transition par défaut n'est pas un simple fondu enchaîné. ::view-transition-group effectue également des transitions:

  • Position et transformation (à l'aide d'un transform)
  • Largeur
  • Taille

Cela n'a pas eu d'importance jusqu'à présent, car l'en-tête a la même taille et se positionne des deux côtés de la modification DOM. Mais vous pouvez également extraire le texte de l'en-tête:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content permet d'ajuster l'élément à la taille du texte au lieu de l'étirer jusqu'à la largeur restante. Sans cela, la flèche de retour réduit la taille de l'élément de texte de l'en-tête au lieu de la même taille dans les deux pages.

Nous avons maintenant trois parties à jouer:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Mais encore une fois, il suffit d'utiliser les valeurs par défaut:

Texte d'en-tête glissant. Démonstration minimale : Source :

Maintenant, le texte du titre fait un glisser un peu satisfaisant pour faire de la place pour le bouton Retour.


Animer plusieurs pseudo-éléments de la même manière avec view-transition-class

Navigateurs pris en charge

  • 125
  • 125
  • x
  • x

Imaginons que vous ayez une transition d'affichage avec un grand nombre de fiches, mais aussi un titre sur la page. Pour animer toutes les fiches à l'exception du titre, vous devez écrire un sélecteur qui cible chaque fiche.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

Vous avez 20 éléments ? Vous devez écrire 20 sélecteurs. Vous souhaitez ajouter un élément ? Ensuite, vous devez aussi agrandir le sélecteur qui applique les styles d'animation. Pas exactement évolutive.

Le view-transition-class peut être utilisé dans les pseudo-éléments de transition de vue pour appliquer la même règle de style.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

L'exemple de fiches suivant exploite l'extrait CSS précédent. Le même moment est appliqué à toutes les fiches, y compris celles qui ont été ajoutées, avec un seul sélecteur: html::view-transition-group(.card).

Enregistrement de la démonstration sur les fiches. L'utilisation de view-transition-class applique le même animation-timing-function à toutes les cartes, à l'exception de celles ajoutées ou supprimées.

Déboguer les transitions

Étant donné que les transitions d'affichage sont basées sur les animations CSS, le panneau Animations des outils pour les développeurs Chrome est idéal pour déboguer les transitions.

À l'aide du panneau Animations, vous pouvez mettre en pause l'animation suivante, puis faire défiler l'animation vers l'avant et l'arrière. Au cours de cette opération, les pseudo-éléments de transition sont disponibles dans le panneau Éléments.

Débogage des transitions de vue avec les outils pour les développeurs Chrome

Les éléments de transition ne doivent pas nécessairement correspondre au même élément DOM.

Jusqu'à présent, nous avons utilisé view-transition-name afin de créer des éléments de transition distincts pour l'en-tête et pour le texte qu'il contient. Il s'agit des mêmes éléments avant et après le changement DOM, mais vous pouvez créer des transitions lorsque ce n'est pas le cas.

Par exemple, l'élément intégré de la vidéo principale peut recevoir un view-transition-name:

.full-embed {
  view-transition-name: full-embed;
}

Ensuite, lorsque l'utilisateur clique sur la miniature, le même view-transition-name peut lui être attribué, juste pour la durée de la transition:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

Et le résultat:

Un élément passant à un autre. Démonstration minimale : Source :

La miniature passe maintenant à l'image principale. Même s'ils sont conceptuellement (et littéralement) différents, l'API de transition les traite comme une seule et même chose, car ils partagent le même view-transition-name.

Le code réel de cette transition est un peu plus compliqué que l'exemple précédent, car il gère également la transition vers la page de vignettes. Consultez la source pour voir l'implémentation complète.


Transitions d'entrée et de sortie personnalisées

Prenons l'exemple suivant:

Ouverture et fermeture de la barre latérale Démonstration minimale : Source :

La barre latérale fait partie de la transition:

.sidebar {
  view-transition-name: sidebar;
}

Mais, contrairement à l'en-tête de l'exemple précédent, la barre latérale n'apparaît pas sur toutes les pages. Si les deux états comportent une barre latérale, les pseudo-éléments de transition se présentent comme suit:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Toutefois, si la barre latérale se trouve uniquement sur la nouvelle page, le pseudo-élément ::view-transition-old(sidebar) ne sera pas présent. Comme il n'existe pas d'"ancienne" image pour la barre latérale, la paire d'images ne comportera qu'une ::view-transition-new(sidebar). De même, si la barre latérale se trouve uniquement sur l'ancienne page, la paire d'images comportera uniquement un ::view-transition-old(sidebar).

Dans la démonstration précédente, la transition de la barre latérale varie selon que la barre change de mode (entrée, sortie ou présence dans les deux états). Elle apparaît en glissant depuis la droite, puis en fondu, en glissant vers la droite et en fondu, et elle reste en place lorsqu'elle est présente dans les deux états.

Pour créer des transitions d'entrée et de sortie spécifiques, vous pouvez utiliser la pseudo-classe :only-child afin de cibler l'ancien ou le nouveau pseudo-éléments lorsqu'il est le seul enfant de la paire d'images:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

Dans ce cas, il n'y a pas de transition spécifique lorsque la barre latérale est présente dans les deux états, car la valeur par défaut est parfaite.

Mises à jour DOM asynchrones et en attente de contenu

Le rappel transmis à .startViewTransition() peut renvoyer une promesse, ce qui permet d'effectuer des mises à jour DOM asynchrones et d'attendre que le contenu important soit prêt.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

La transition ne commencera pas tant que la promesse n'aura pas été effectuée. Pendant cette période, la page est bloquée. Vous devez donc réduire au maximum les délais. Plus précisément, les extractions réseau doivent être effectuées avant d'appeler .startViewTransition(), lorsque la page reste entièrement interactive, plutôt que de les faire dans le cadre du rappel .startViewTransition().

Si vous décidez d'attendre que les images ou les polices soient prêtes, veillez à utiliser un délai avant expiration agressif:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

Toutefois, dans certains cas, il est préférable d'éviter tout retard et d'utiliser le contenu dont vous disposez déjà.


Exploitez tout le potentiel de vos contenus existants

Si la miniature passe à une image plus grande:

Vignette passant à une image plus grande. Essayez le site de démonstration.

La transition par défaut est un fondu enchaîné, ce qui signifie que le fondu enchaîné de la miniature est peut-être dû à une image complète qui n'a pas encore été chargée.

Pour gérer ce problème, vous pouvez attendre que l'image soit entièrement chargée avant de lancer la transition. Idéalement, cette opération devrait être effectuée avant d'appeler .startViewTransition(), afin que la page reste interactive et qu'une icône de chargement puisse s'afficher pour indiquer à l'utilisateur que les éléments sont en cours de chargement. Mais dans ce cas, il existe une meilleure solution:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

La vignette ne disparaît pas, elle reste simplement sous l'image entière. Cela signifie que si la nouvelle vue n'a pas été chargée, la vignette reste visible pendant la transition. Cela signifie que la transition peut commencer immédiatement et que l'image complète peut se charger à son tour.

Cela ne fonctionnerait pas si la nouvelle vue mettait en avant la transparence, mais dans le cas présent, nous savons que ce n'est pas le cas. Nous pouvons donc effectuer cette optimisation.

Gérer les modifications de format

Pratiquement, jusqu'à présent, toutes les transitions ont été effectuées vers des éléments ayant le même format, mais ce ne sera pas toujours le cas. Que se passe-t-il si la miniature est au format 1:1 et que l'image principale est au format 16:9 ?

Un élément passant à un autre, avec un changement de format. Démonstration minimale : Source :

Dans la transition par défaut, le groupe s'anime de la taille "avant" à la taille "après". L'ancienne et la nouvelle vue ont une largeur de 100% du groupe, et la hauteur est automatique, ce qui signifie qu'elles conservent leurs proportions, quelle que soit la taille du groupe.

Cette valeur par défaut est correcte, mais elle ne correspond pas à celle souhaitée dans ce cas. Comme suit :

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

Cela signifie que la vignette reste au centre de l'élément lorsque la largeur augmente, mais que l'image entière est "recadrée" lors du passage du format 1:1 au format 16:9.

Pour en savoir plus, consultez Afficher les transitions: gérer les modifications de format.


Utiliser des requêtes média pour modifier les transitions en fonction de l'état de l'appareil

Vous pouvez utiliser des transitions différentes sur mobile et sur ordinateur, comme dans cet exemple, qui affiche une diapositive complète sur le côté mobile et une diapositive plus subtile sur un ordinateur de bureau:

Un élément passant à un autre. Démonstration minimale : Source :

Pour ce faire, utilisez des requêtes média standards:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

Vous pouvez également modifier les éléments auxquels vous attribuez une view-transition-name en fonction des requêtes média correspondantes.


Réagir à la préférence "Mouvement réduit"

Les utilisateurs peuvent indiquer qu'ils préfèrent les mouvements réduits via leur système d'exploitation, et cette préférence est exposée dans CSS.

Vous pouvez choisir d'empêcher toute transition pour ces utilisateurs:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Toutefois, une préférence pour une "réduction du mouvement" ne signifie pas que l'utilisateur ne souhaite aucun mouvement. Au lieu de l'extrait précédent, vous pouvez choisir une animation plus subtile, mais qui exprime tout de même la relation entre les éléments et le flux de données.


Gérer plusieurs styles de transition de vue avec des types de transition de vue

Parfois, une transition d'une vue particulière à une autre doit être spécifiquement adaptée. Par exemple, lorsque vous passez à la page suivante ou précédente dans une séquence de pagination, vous pouvez faire glisser le contenu dans une direction différente selon que vous accédez à une page située plus bas ou plus haut dans la séquence.

Enregistrement de la démonstration de Pagination. Elle utilise différentes transitions selon la page que vous consultez.

Pour ce faire, vous pouvez utiliser des types de transition "Vue", qui vous permettent d'attribuer un ou plusieurs types à une transition Active View. Par exemple, lorsque vous passez à une page supérieure dans une séquence de pagination, utilisez le type forwards et lorsque vous passez à une page inférieure, utilisez le type backwards. Ces types ne sont actifs que lors de la capture ou de l'exécution d'une transition, et ils peuvent être personnalisés via CSS afin d'utiliser des animations différentes.

Pour utiliser des types dans une transition de vue d'un même document, transmettez types à la méthode startViewTransition. Pour ce faire, document.startViewTransition accepte également un objet: update est la fonction de rappel qui met à jour le DOM, et types est un tableau contenant les types.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

Pour répondre à ces types, utilisez le sélecteur :active-view-transition-type(). Transmettez le type que vous souhaitez cibler au sélecteur. Cela vous permet de séparer les styles de plusieurs transitions de vue les uns des autres, sans que les déclarations de l'une n'interfèrent avec celles de l'autre.

Étant donné que les types ne s'appliquent que lors de la capture ou de l'exécution de la transition, vous pouvez utiliser le sélecteur pour définir (ou non) une view-transition-name sur un élément uniquement pour la transition de vue avec ce type.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

Dans la démonstration de la pagination suivante, le contenu de la page glisse vers l'avant ou vers l'arrière en fonction du numéro de la page à laquelle vous accédez. Les types sont déterminés au moment du clic sur lequel ils sont transmis à document.startViewTransition.

Pour cibler n'importe quelle transition Active View, quel que soit son type, vous pouvez utiliser le sélecteur de pseudo-classe :active-view-transition à la place.

html:active-view-transition {
    …
}

Gérer plusieurs styles de transition de vue avec un nom de classe à la racine de la transition de vue

Parfois, une transition d'un type de vue particulier à un autre doit être spécifiquement adaptée. Une navigation "Retour" doit également être différente d'une navigation "avant".

Différentes transitions lorsque vous revenez en arrière. Démonstration minimale : Source :

Avant les types de transition, la façon de gérer ces cas de figure consistait à définir temporairement un nom de classe à la racine de la transition. Lors de l'appel de document.startViewTransition, cette racine de transition correspond à l'élément <html>, accessible à l'aide de document.documentElement en JavaScript:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

Pour supprimer les classes une fois la transition terminée, cet exemple utilise transition.finished, une promesse qui se résout une fois la transition atteint son état final. Les autres propriétés de cet objet sont présentées dans la documentation de référence de l'API.

Vous pouvez maintenant utiliser ce nom de classe dans votre CSS pour modifier la transition:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

Comme pour les requêtes média, la présence de ces classes peut également être utilisée pour modifier les éléments qui obtiennent un view-transition-name.


Exécuter des transitions sans figer les autres animations

Regardez cette démo d’un poste de transition vidéo:

Transition vidéo. Démonstration minimale : Source :

Avez-vous remarqué quelque chose de mal ? Ne vous inquiétez pas si ce n’est pas le cas. Ici, elle est tout de suite ralentie:

Transition vidéo, plus lente. Démonstration minimale : Source :

Pendant la transition, la vidéo semble se figer, puis la version en cours de lecture apparaît en fondu. En effet, ::view-transition-old(video) est une capture d'écran de l'ancienne vue, tandis que ::view-transition-new(video) est une image active de la nouvelle vue.

Vous pouvez résoudre ce problème, mais commencez par vous demander si cela vaut la peine d'être corrigé. Si ce "problème" ne s'affichait pas alors que la transition était en cours de lecture à sa vitesse normale, je ne changerais pas la configuration.

Si vous voulez vraiment corriger le problème, n'affichez pas le ::view-transition-old(video). Passez directement à ::view-transition-new(video). Pour ce faire, remplacez les styles et animations par défaut:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

Voilà, c'est terminé !

Transition vidéo, plus lente. Démonstration minimale : Source :

La vidéo est alors lue tout au long de la transition.


Animation avec JavaScript

Jusqu'à présent, toutes les transitions ont été définies à l'aide de CSS, mais il arrive que le CSS ne suffit pas:

Transition de cercle. Démonstration minimale : Source :

Quelques éléments de cette transition ne peuvent pas être effectués seul avec le CSS:

  • L'animation commence à l'emplacement du clic.
  • L'animation se termine avec le cercle ayant un rayon jusqu'à l'angle le plus éloigné. Toutefois, cela devrait être possible avec CSS à l'avenir.

Heureusement, vous pouvez créer des transitions à l'aide de l'API Web Animation.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

Cet exemple utilise transition.ready, une promesse qui se résout une fois les pseudo-éléments de transition créés. Les autres propriétés de cet objet sont présentées dans la documentation de référence de l'API.


Les transitions comme amélioration

L'API View Transition est conçue pour encapsuler une modification DOM et créer une transition pour celle-ci. Toutefois, la transition doit être traitée comme une amélioration. En d'autres termes, votre application ne doit pas passer à l'état "erreur" si la modification DOM aboutit, mais qu'elle échoue. Dans l'idéal, la transition ne devrait pas échouer. Si c'est le cas, cela ne devrait pas affecter le reste de l'expérience utilisateur.

Afin de traiter les transitions comme une amélioration, veillez à ne pas utiliser les promesses de transition d'une manière qui pourrait entraîner le dysfonctionnement de votre application en cas d'échec de la transition.

À éviter
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

Le problème avec cet exemple est que switchView() rejette un état si la transition ne peut pas atteindre un état ready, mais cela ne signifie pas que le changement de vue a échoué. Le DOM a peut-être bien été mis à jour, mais la transition a été ignorée, car il y avait des view-transition-name en double.

Au lieu de cela :

À faire
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

Cet exemple utilise transition.updateCallbackDone pour attendre la mise à jour DOM et la refuser en cas d'échec. switchView ne refuse plus la transition en cas d'échec de la transition, se résout une fois la mise à jour DOM terminée et la refuse en cas d'échec.

Si vous souhaitez que switchView soit résolu lorsque la nouvelle vue est "réglée", comme c'est le cas pour toute transition animée, remplacez transition.updateCallbackDone par transition.finished.


Ce n'est pas un polyfill, mais...

Ce n'est pas une fonctionnalité facile à émuler. Toutefois, cette fonction d'assistance facilite grandement les opérations dans les navigateurs qui ne prennent pas en charge les transitions de vue:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

Et il peut être utilisé comme ceci:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

Dans les navigateurs qui ne sont pas compatibles avec les transitions de vue, updateDOM sera toujours appelé, mais aucune transition ne sera animée.

Vous pouvez également fournir des classNames à ajouter à <html> pendant la transition, ce qui vous permettra de modifier plus facilement la transition en fonction du type de navigation.

Vous pouvez également transmettre true à skipTransition si vous ne souhaitez pas d'animation, même dans les navigateurs compatibles avec les transitions de vue. Cette option est utile si les utilisateurs de votre site préfèrent désactiver les transitions.


Travailler avec des cadres

Si vous travaillez avec une bibliothèque ou un framework qui élimine les modifications DOM, le plus difficile est de savoir quand ces modifications sont terminées. Voici un ensemble d'exemples, qui utilisent l'outil d'aide ci-dessus, dans différents frameworks.

  • React : la clé ici est flushSync, qui applique un ensemble de changements d'état de manière synchrone. Oui, l'utilisation de cette API nécessite un avertissement important, mais Dan Abramov assure qu'elle est appropriée dans ce cas. Comme d'habitude avec React et le code asynchrone, lorsque vous utilisez les différentes promesses renvoyées par startViewTransition, assurez-vous que votre code s'exécute avec l'état approprié.
  • Vue.js : il s'agit ici de nextTick, qui s'exécute une fois le DOM mis à jour.
  • Svelte : très semblable à Vue, mais la méthode pour attendre le prochain changement est tick.
  • Lit : la clé ici est la promesse this.updateComplete dans les composants, qui s'exécute une fois le DOM mis à jour.
  • Angular : la clé ici est applicationRef.tick, qui supprime les modifications DOM en attente. À partir de la version 17 d'Angular, vous pouvez utiliser le withViewTransitions fourni avec @angular/router.

Documentation de référence de l'API

const viewTransition = document.startViewTransition(update)

Commencez une nouvelle ViewTransition.

update est une fonction appelée une fois que l'état actuel du document a été capturé.

Ensuite, lorsque la promesse renvoyée par updateCallback est exécutée, la transition commence dans le frame suivant. Si la promesse renvoyée par updateCallback est refusée, la transition est abandonnée.

const viewTransition = document.startViewTransition({ update, types })

Démarrer un nouveau ViewTransition avec les types spécifiés

update est appelé une fois l'état actuel du document capturé.

types définit les types actifs pour la transition lors de la capture ou de l'exécution de la transition. Il est initialement vide. Pour en savoir plus, consultez la section viewTransition.types ci-dessous.

Membres de l'instance de ViewTransition:

viewTransition.updateCallbackDone

Promesse qui est exécutée lorsque la promesse renvoyée par updateCallback est exécutée, ou rejetée lorsqu'elle est refusée.

L'API View Transition encapsule une modification DOM et crée une transition. Cependant, il arrive parfois que la réussite ou l'échec de l'animation de transition ne vous préoccupe pas. Il vous suffit de savoir si et quand le changement DOM se produit. updateCallbackDone est destiné à ce cas d'utilisation.

viewTransition.ready

Promesse qui est exécutée une fois que les pseudo-éléments de la transition sont créés et que l'animation est sur le point de commencer.

Il est refusé si la transition ne peut pas commencer. Cela peut être dû à une mauvaise configuration, telle que des view-transition-name en double, ou si updateCallback renvoie une promesse refusée.

Cela s'avère utile pour animer les pseudo-éléments de transition avec JavaScript.

viewTransition.finished

Promesse qui se tient une fois que l'état final est entièrement visible et interactif pour l'utilisateur.

Elle ne rejette que si updateCallback renvoie une promesse refusée, car cela indique que l'état final n'a pas été créé.

Sinon, si une transition ne démarre pas ou est ignorée pendant la transition, l'état final est quand même atteint. finished est donc exécuté.

viewTransition.types

Objet de type Set contenant les types de transition Active View. Pour manipuler les entrées, utilisez ses méthodes d'instance clear(), add() et delete().

Pour répondre à un type spécifique en CSS, utilisez le sélecteur de pseudo-classe :active-view-transition-type(type) sur la racine de la transition.

Les types sont automatiquement nettoyés une fois la transition d'affichage terminée.

viewTransition.skipTransition()

Ignorez la partie animation de la transition.

L'appel de updateCallback ne sera pas ignoré, car la modification DOM est distincte de la transition.


Référence de style et de transition par défaut

::view-transition
Pseu-élément racine qui remplit la fenêtre d'affichage et contient chaque ::view-transition-group.
::view-transition-group

Tout à fait.

Passage de width et height entre les états "avant" et "après".

Passe transform entre le quad d'espace d'affichage de la fenêtre d'affichage "avant" et "après".

::view-transition-image-pair

Il est tout à fait positionné pour remplir le groupe.

Permet de définir isolation: isolate pour limiter l'effet de mix-blend-mode sur l'ancienne et la nouvelle vue.

::view-transition-new et ::view-transition-old

Il se trouve tout en haut à gauche du wrapper.

Remplit 100% de la largeur du groupe, mais sa hauteur est définie automatiquement, ce qui permet de conserver ses proportions au lieu de remplir le groupe.

Contient mix-blend-mode: plus-lighter pour permettre un vrai fondu enchaîné.

L'ancienne vue passe de opacity: 1 à opacity: 0. La nouvelle vue passe de opacity: 0 à opacity: 1.


Commentaires

Les commentaires des développeurs sont toujours les bienvenus. Pour ce faire, signalez un problème au groupe de travail CSS sur GitHub en lui faisant part de suggestions et de questions. Ajoutez un préfixe [css-view-transitions] à votre problème.

Si vous rencontrez un bug, signalez-le plutôt.