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

Publié le 17 août 2021 – Dernière mise à jour : 25 septembre 2024

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, Single Page Application), où JavaScript est utilisé pour mettre à jour le DOM. Les transitions de vue dans le même document sont compatibles avec Chrome à partir de la version 111.

Pour déclencher une transition de vue du 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, puis prend des instantanés du nouvel état.

Ces instantanés sont ensuite organisés dans un arbre de pseudo-éléments et animés à l'aide des puissantes 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 le CSS pour personnaliser les animations.


Transition par défaut : fondu croisé

La transition de vue par défaut est un fondu croisé. Elle constitue donc une bonne 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 définit le DOM sur le 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é:

Floutage par défaut. Une démo minimale : Source.

Un fondu enchaîné n'est pas si impressionnant. Heureusement, les transitions peuvent être personnalisées. Mais vous devez d'abord comprendre comment fonctionne ce fondu croisé de base.


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'un instantané.

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éments comme suit :

::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, et ::view-transition-new(root) est une représentation en direct de la nouvelle vue. Les deux s'affichent sous la forme de "contenu remplacé" CSS (comme un <img>).

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

Toutes les animations sont effectuées à l'aide d'animations CSS. Elles peuvent donc être personnalisées avec CSS.

Personnaliser la transition

Tous les pseudo-éléments de transition d'affichage peuvent être ciblés avec CSS. Étant donné que 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:

<ph type="x-smartling-placeholder">
</ph>
Un fondu enchaîné long. Démonstration minimale Source :

OK, ce n'est toujours pas impressionnant. À la place, le code suivant implémente la transition d'axe partagée 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;
}

Voici le résultat :

Transition sur un axe partagé. Démonstration minimale Source :

Faire passer plusieurs éléments en transition

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 ne semble pas tout à fait adapté pour l'en-tête, car il glisse vers l'extérieur, puis vers 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;
}

La valeur de view-transition-name peut être celle de votre choix (sauf none, ce qui signifie qu'il n'y a pas de nom de transition). Il permet d'identifier de manière unique l'élément pendant la transition.

Le résultat est le suivant :

<ph type="x-smartling-placeholder">
</ph>
Transition "Axe partagé" avec en-tête fixe. Une démo minimale : Source :

L'en-tête reste maintenant en place et se fond dans le nouveau contenu.

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. Ils peuvent être ciblés indépendamment avec CSS et recevoir 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 seulement un fondu croisé. ::view-transition-group effectue également des transitions :

  • Positionner et transformer (à l'aide d'un transform)
  • Largeur
  • Hauteur

Cela n'avait pas d'importance jusqu'à présent, car l'en-tête est de la même taille et de la même position de chaque côté du changement de DOM. 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 de faire en sorte que l'élément ait la taille du texte, au lieu de s'étendre sur la largeur restante. Sinon, la flèche de retour réduit la taille de l'élément de texte de l'en-tête, au lieu de la maintenir identique sur 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 de l'en-tête coulissant. Une démo minimale : Source :

Le texte du titre glisse maintenant pour laisser de l'espace au bouton Retour.


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

Navigateurs pris en charge

  • Chrome: 125
  • Edge: 125
  • Firefox : non compatible.
  • Safari Technology Preview : compatible.

Imaginons que vous ayez une transition de vue avec un certain 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 donc écrire 20 sélecteurs. Ajouter un élément ? Ensuite, vous devez aussi agrandir le sélecteur qui applique les styles d'animation. Pas exactement évolutive.

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 cartes 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 des cartes. 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 de vue sont basées sur des animations CSS, le panneau Animations des outils pour les développeurs Chrome est idéal pour déboguer les transitions.

Dans le panneau Animations, vous pouvez mettre en pause l'animation suivante, puis la faire avancer et reculer. Les pseudo-éléments de transition se trouvent alors dans le panneau Éléments.

<ph type="x-smartling-placeholder">
</ph>
Déboguer les transitions de vue avec les outils pour les développeurs Chrome

Les éléments en transition ne doivent pas nécessairement être le 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 du même élément conceptuellement avant et après le changement de DOM, mais vous pouvez créer des transitions où ce n'est pas le cas.

Par exemple, vous pouvez attribuer un view-transition-name à l'intégration vidéo principale :

.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 :

<ph type="x-smartling-placeholder">
</ph>
Un élément passant à un autre. Démonstration minimale Source :

La vignette passe maintenant à l'image principale. Même s'il s'agit d'éléments 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 complexe que l'exemple précédent, car il gère également la transition vers la page de miniatures. 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. Une démo minimale : Source.

La barre latérale fait partie de la transition :

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

Toutefois, 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 ne se trouve que sur la nouvelle page, le pseudo-élément ::view-transition-old(sidebar) n'y apparaîtra pas. Puisqu'il n'y a pas de « ancien » pour la barre latérale, la paire d'images ne comportera que ::view-transition-new(sidebar). De même, si la barre latérale ne se trouve que sur l'ancienne page, la paire d'images ne comporte qu'un ::view-transition-old(sidebar).

Dans la démonstration précédente, la barre latérale passe d'un état à un autre différemment selon qu'elle apparaît, disparaît ou est présente dans les deux états. Il apparaît en glissant de droite à gauche et en s'éclaircissant, disparaît en glissant de gauche à droite et en s'assombrissant, et reste en place lorsqu'il est présent 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 asynchrones du DOM et attente du 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 ce temps, la page est figée. Les retards doivent donc être réduits au minimum. Plus précisément, les extractions réseau doivent être effectuées avant d'appeler .startViewTransition(), lorsque la page est toujours entièrement interactive, plutôt que de le 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 vignette passe à une image plus grande :

<ph type="x-smartling-placeholder">
</ph>
Vignette qui passe à une image plus grande. Essayez le site de démonstration.

La transition par défaut est un fondu enchaîné, ce qui signifie que la vignette peut être fondue avec une image complète qui n'a pas encore été chargée.

Pour y remédier, vous pouvez attendre que l'image complète soit 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 démarrer immédiatement et que l'image complète peut se charger à son tour.

Cela ne fonctionnerait pas si la nouvelle vue comportait de la transparence, mais dans ce cas, nous savons qu'elle n'en comporte pas, ce qui nous permet d'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 vignette est au format 1:1 et que l'image principale est au format 16:9 ?

Un élément passe à un autre, avec un changement de format. Une démo minimale : Source.

Dans la transition par défaut, le groupe passe de la taille d'avant à la taille d'après. Les anciennes et nouvelles vues occupent 100 % de la largeur du groupe et une hauteur automatique, ce qui signifie qu'elles conservent leur format quelle que soit la taille du groupe.

Il s'agit d'une bonne valeur par défaut, mais ce n'est pas ce que vous souhaitez dans ce cas. Exemple :

::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 complète est "découpée" lorsqu'elle passe de 1:1 à 16:9.

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


Utiliser des requêtes multimédias pour modifier les transitions en fonction des différents états de l'appareil

Vous pouvez utiliser des transitions différentes sur mobile et sur ordinateur. Par exemple, dans cet exemple, une diapositive complète est effectuée sur le côté sur mobile, mais une diapositive plus subtile sur ordinateur :

Un élément passe à un autre. Une démo minimale : Source :

Pour ce faire, vous pouvez utiliser des requêtes multimédias 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 un view-transition-name en fonction des requêtes multimédias correspondantes.


Réagir à la "réduction du mouvement" préférence

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;
  }
}

Cependant, une préférence pour « mouvements réduits » ne signifie pas que l'utilisateur ne veut aucun mouvement. Au lieu de l'extrait précédent, vous pouvez choisir une animation plus subtile, mais qui exprime toujours 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

Navigateurs pris en charge

  • Chrome : 125.
  • Edge: 125
  • Firefox: non compatible.
  • Safari: 18.

Parfois, une transition d'une vue à une autre doit être adaptée spécifiquement. 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 la pagination. Il utilise différentes transitions selon la page à laquelle vous accédez.

Pour ce faire, vous pouvez utiliser des types de transition de vue, qui vous permettent d'attribuer un ou plusieurs types à une transition de vue active. 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. Chacun d'eux peut être personnalisé à l'aide de CSS pour utiliser différentes animations.

Pour utiliser des types dans une transition de vue du même document, vous 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 l'type que vous souhaitez cibler dans le sélecteur. Vous pouvez ainsi séparer les styles de plusieurs transitions de vue les uns des autres, sans que les déclarations de l'un 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) un 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 pagination suivante, le contenu de la page glisse vers l'avant ou vers l'arrière en fonction du numéro de page vers lequel vous naviguez. Les types sont déterminés au moment du clic, puis transmis à document.startViewTransition.

Pour cibler n'importe quelle transition de vue active, quel que soit le type, vous pouvez utiliser le sélecteur de pseudo-classe :active-view-transition.

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. Par exemple, la navigation "Retour" doit être différente de la navigation "Avancer".

Différentes transitions lorsque vous revenez en arrière. Une démo minimale : Source.

Avant les types de transition, la méthode permettant de gérer ces cas consistait à définir temporairement un nom de classe sur 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 que la transition a atteint son état final. Les autres propriétés de cet objet sont décrites 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 d'autres animations

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

<ph type="x-smartling-placeholder">
</ph>
Transition vidéo. Démonstration minimale Source.

Avez-vous remarqué quelque chose de mal ? Ne vous inquiétez pas si vous ne l'avez pas fait. 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 lue de la vidéo apparaît progressivement. 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 vous n'avez pas vu le "problème" lorsque la transition était diffusée à sa vitesse normale, je ne m'embêterais pas à la modifier.

Si vous voulez vraiment corriger le problème, n'affichez pas ::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;
}

Et voilà !

<ph type="x-smartling-placeholder">
</ph>
Transition vidéo, plus lente. Démonstration minimale Source :

La vidéo est maintenant lue pendant toute la transition.


Intégration à l'API Navigation (et à d'autres frameworks)

Les transitions de vue sont spécifiées de manière à pouvoir être intégrées à d'autres frameworks ou bibliothèques. Par exemple, si votre application monopage utilise un routeur, vous pouvez ajuster le mécanisme de mise à jour du routeur pour mettre à jour le contenu à l'aide d'une transition de vue.

Dans l'extrait de code suivant, extrait de cette démonstration de pagination, le gestionnaire d'interception de l'API Navigation est ajusté pour appeler document.startViewTransition lorsque les transitions de vue sont prises en charge.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

Certains navigateurs offrent leur propre transition lorsque l'utilisateur effectue un geste de balayage pour naviguer. Dans ce cas, vous ne devez pas déclencher votre propre transition de vue, car cela entraînerait une expérience utilisateur mauvaise ou déroutante. L'utilisateur verrait deux transitions s'exécuter successivement, l'une fournie par le navigateur et l'autre par vous.

Par conséquent, nous vous recommandons d'empêcher le démarrage d'une transition de vue lorsque le navigateur a fourni sa propre transition visuelle. Pour ce faire, vérifiez la valeur de la propriété hasUAVisualTransition de l'instance NavigateEvent. La propriété est définie sur true lorsque le navigateur a fourni une transition visuelle. Cette propriété hasUIVisualTransition existe également sur PopStateEvent instances.

Dans l'extrait précédent, la vérification qui détermine si la transition de vue doit être exécutée prend cette propriété en compte. Lorsque les transitions de vue dans le même document ne sont pas prises en charge ou lorsque le navigateur a déjà fourni sa propre transition, la transition de vue est ignorée.

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

Dans l'enregistrement suivant, l'utilisateur balaie l'écran pour revenir à la page précédente. La capture de gauche ne vérifie pas l'indicateur hasUAVisualTransition. L'enregistrement de droite inclut la vérification, ce qui permet de passer outre la transition manuelle de vue, car le navigateur a fourni une transition visuelle.

<ph type="x-smartling-placeholder">
</ph>
Comparaison d'un même site sans (à gauche) et en largeur (à droite) une vérification de hasUAVisualTransition

Animer 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 "Cercle". Une démo minimale : Source.

Quelques parties de cette transition ne peuvent pas être effectuées uniquement avec le CSS:

  • L'animation commence à partir du point de clic.
  • L'animation se termine avec le cercle ayant un rayon jusqu'à l'angle le plus éloigné. Nous espérons que cela sera 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 décrites dans la documentation de référence de l'API.


Transitions en tant qu'amélioration

L'API View Transition est conçue pour "encapsuler" une modification DOM et créer une transition pour celle-ci. Cependant, la transition doit être considérée comme une amélioration, car votre application ne doit pas saisir d'erreur. si la modification DOM aboutit, mais que la transition échoue. Dans l'idéal, la transition ne doit pas échouer, mais si elle le fait, elle ne doit pas interrompre le reste de l'expérience utilisateur.

Pour traiter les transitions comme une amélioration, veillez à ne pas utiliser les promesses de transition de manière à provoquer une erreur dans votre application si la transition échoue.

À é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 de cet exemple est que switchView() rejette la transition si celle-ci 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 se résout lorsque la nouvelle vue est "stabilisée", c'est-à-dire qu'une transition animée est terminée ou a été ignorée jusqu'à la fin, remplacez transition.updateCallbackDone par transition.finished.


Pas un polyfill, mais…

Il ne s'agit pas d'une fonctionnalité facile à polyfiller. 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');
  }
}

Vous pouvez l'utiliser comme suit :

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 permet de modifier la transition en fonction du type de navigation plus facilement.

Vous pouvez également transmettre true à skipTransition si vous ne souhaitez pas d'animation, même dans les navigateurs compatibles avec les transitions de vue. Cela est utile si votre site propose aux utilisateurs de désactiver les transitions.


Travailler avec des cadres

Si vous utilisez une bibliothèque ou un framework qui abstrait les modifications DOM, la partie délicate consiste à savoir quand la modification DOM est terminée. Voici un ensemble d'exemples, utilisant l'aide ci-dessus, dans différents frameworks.

  • React : la clé ici est flushSync, qui applique un ensemble de modifications d'état de manière synchrone. Oui, il existe un avertissement important concernant l'utilisation de cette API, mais Dan Abramov m'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, veillez à ce 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)

Créez un 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 remplie, 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 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 du 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 s'exécute 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, par exemple des view-transition-name en double, ou si updateCallback renvoie une promesse refusée.

Cela est 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.

Il n'est refusé 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 commence pas ou est ignorée pendant la transition, l'état final est toujours atteint, de sorte que finished est rempli.

viewTransition.types

Objet de type Set contenant les types de transition Active View. Pour manipuler les entrées, utilisez les 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érences sur le style et la 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

Positionnement absolu.

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

Ajoute une transition de transform entre "avant" et "après" quadruple espace d'affichage.

::view-transition-image-pair

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

Possède isolation: isolate pour limiter l'effet de mix-blend-mode sur les anciennes et nouvelles vues.

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

Elle se trouve tout en haut à gauche du wrapper.

Occupe 100 % de la largeur du groupe, mais a une hauteur automatique. Il conserve donc son format 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 indiquant vos suggestions et questions. Ajoutez un préfixe [css-view-transitions] à votre problème.

Si vous rencontrez un bug, signalez-le dans Chromium.