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

Lorsqu'une transition d'affichage s'exécute sur un seul document, on parle de transition de vue d'un 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 d'un même document sont possibles dans Chrome à partir de Chrome 111.

Pour déclencher une transition d'affichage d'un 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 auxquels 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 en arborescence de pseudo-éléments et animés grâce aux 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 se fond en fondu. Si vous le souhaitez, vous pouvez utiliser du code CSS pour personnaliser les animations.


Transition par défaut: fondu enchaîné

La transition de vue par défaut est un fondu enchaîné et 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 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 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é 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é. L'API capture ensuite le nouvel état de la page.

Une fois le nouvel état capturé, l'API construit une arborescence de pseudo-éléments comme ceci:

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

Le ::view-transition se trouve dans une superposition, par-dessus tout le reste de la page. Cette option est 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. Ces deux formats s'affichent sous la forme d'un "contenu remplacé" CSS (comme <img>).

L'ancienne vue s'anime de opacity: 1 à opacity: 0, tandis que la nouvelle s'anime de opacity: 0 à opacity: 1, créant ainsi 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 à l'aide de CSS. Les animations étant 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 cette seule modification, le fondu est désormais 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 autour de l'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 vers un axe partagé. Démonstration minimale : Source

Ajouter une transition entre plusieurs éléments

Dans la démo précédente, l'ensemble de la page est impliqué 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 le titre, car il glisse simplement pour glisser à nouveau.

Pour éviter cela, vous pouvez extraire l'en-tête du reste de la page afin qu'il puisse être animé 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 définie comme vous le souhaitez (sauf none, qui signifie qu'il n'y a pas de nom de transition). Il permet d'identifier de manière unique l'élément tout au long de la transition.

Et le résultat:

Transition de l'axe partagé avec en-tête fixe. Démonstration minimale : Source

L'en-tête reste en place et se fond en fondu.

Cette déclaration CSS a entraîné la modification de l'arborescence de 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 désormais deux groupes de transition. une pour l'en-tête et une autre pour le reste. Vous pouvez les cibler indépendamment avec le CSS et utiliser différentes transitions. Toutefois, dans ce cas, main-header a conservé la transition par défaut, qui est un fondu enchaîné.

La transition par défaut n'est pas qu'un fondu enchaîné. La transition de ::view-transition-group se fait également:

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

Ce n'était pas important jusqu'à présent, car l'en-tête a la même taille et la position des deux côtés du DOM change. Toutefois, 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 est utilisé pour que l'élément corresponde à la taille du texte, plutôt que d'être étiré selon 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, plutôt que la même taille sur les deux pages.

Nous devons maintenant nous pencher sur trois aspects:

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

Là encore, gardez les valeurs par défaut:

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

Maintenant, le texte du titre fait un petit glissement 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

Supposons que vous ayez une transition de vue avec plusieurs fiches, mais aussi un titre sur la page. Pour animer toutes les fiches (sauf le titre), vous devez écrire un sélecteur qui cible chacune d'elles.

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 ? Cela fait 20 sélecteurs à écrire. Vous souhaitez ajouter un élément ? Vous devez également développer le sélecteur qui applique les styles d'animation. Pas exactement évolutif.

view-transition-class peut être utilisé dans les pseudo-éléments de transition d'affichage 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 fiche suivant exploite l'extrait CSS précédent. Toutes les fiches, y compris celles qui viennent d'être ajoutées, obtiennent le même code temporel avec un seul sélecteur: html::view-transition-group(.card).

Enregistrement de la démonstration des fiches Si vous utilisez view-transition-class, le même animation-timing-function s'applique à toutes les cartes, sauf celles ajoutées ou supprimées.

Déboguer les transitions

Les transitions de vue étant basées sur des animations CSS, le panneau Animations des outils pour les développeurs Chrome est idéal pour déboguer les transitions.

Le panneau Animations vous permet de mettre en pause l'animation suivante, puis d'utiliser la barre de lecture pour parcourir l'animation. Les pseudo-éléments de transition sont disponibles dans le panneau Éléments pendant cette opération.

Déboguer les transitions de vue à l'aide des outils pour les développeurs Chrome

Les éléments de 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 le texte de l'en-tête. D'un point de vue conceptuel, il s'agit du même élément avant et après le changement DOM, mais vous pouvez créer des transitions là où ce n'est pas le cas.

Par exemple, l'élément vidéo intégré principal 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é, uniquement pour la durée de la transition:

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

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

Et voici le résultat:

Passage d'un élément à un autre. Démonstration minimale : Source

La miniature devient l'image principale. Même s'il s'agit d'éléments conceptuels (et littéralement) différents, l'API de transition les traite comme la 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 le retour à la page de vignettes. Consultez la source pour voir l'implémentation complète.


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

Prenons cet exemple:

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

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) ne sera pas présent. Étant donné qu'il n'y a pas de "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 ne se trouve que sur l'ancienne page, la paire d'images ne comportera qu'un ::view-transition-old(sidebar).

Dans la démonstration précédente, les transitions de la barre latérale varient selon qu'elle est affichée, qu'elle se ferme ou qu'elle est présente dans les deux états. Elle y accède en glissant de droite et en fondu, sort en glissant vers la droite et en fondu, et 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 les anciens ou nouveaux pseudo-éléments lorsqu'il s'agit du 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 en attente de contenu

Le rappel transmis à .startViewTransition() peut renvoyer une promesse, ce qui permet d'effectuer des mises à jour asynchrones du DOM 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'a pas été remplie. Pendant ce temps, la page est figée. Les retards doivent donc être minimisés. Plus précisément, les récupérations de réseau doivent être effectuées avant d'appeler .startViewTransition(), lorsque la page est toujours entièrement interactive, plutôt que dans le rappel .startViewTransition().

Si vous décidez d'attendre que les images ou les polices soient prêtes, assurez-vous de définir un délai d'inactivité strict:

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 des contenus dont vous disposez déjà

Si la miniature passe à une image plus grande:

Transition de la vignette vers une image plus grande. Essayez le site de démonstration.

Par défaut, la transition s'effectue en fondu enchaîné, ce qui signifie que la vignette peut être en fondu enchaîné avec une image complète qui n'est pas encore chargée.

Pour ce faire, vous pouvez attendre que l'image complète soit chargée avant de lancer la transition. Idéalement, cette opération doit être effectuée avant d'appeler .startViewTransition(), afin que la page reste interactive et qu'une icône de chargement puisse être affichée pour indiquer à l'utilisateur que le chargement est en cours. 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 miniature ne disparaît pas : elle est simplement placée sous l'image complète. 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 à sa propre vitesse.

Si la nouvelle vue présentait un niveau de transparence, cela ne fonctionnerait pas, mais dans le cas présent, nous savons que ce n'est pas le cas, nous pouvons donc effectuer cette optimisation.

Gérer les changements de format

En pratique, toutes les transitions jusqu'à présent ont été appliquées à 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 ?

Transition d'un élément vers 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 affichent la largeur du groupe à 100 %, et la hauteur automatique, ce qui signifie qu'elles conservent leurs proportions, quelle que soit la taille du groupe.

C'est une bonne valeur par défaut, mais ce n'est pas ce qui est voulu 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 miniature reste au centre de l'élément lorsque la largeur s'agrandit, mais l'image complète n'est plus recadrée lorsqu'elle passe du format 1:1 au format 16:9.

Pour en savoir plus, consultez (Afficher les transitions: Gérer les changements de format)(https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/)


Utiliser des requêtes multimédias pour modifier les transitions selon l'état de l'appareil

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

Passage d'un élément à un autre. Démonstration minimale : Source

Pour ce faire, utilisez les requêtes média classiques:

/* 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 de réduction des mouvements

Les utilisateurs peuvent indiquer qu'ils préfèrent réduire les mouvements via leur système d'exploitation. Cette préférence est exposée dans le 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 "réduction des mouvements" ne signifie pas que l'utilisateur 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 vues à l'aide de types de transition de vues

Parfois, une transition d'un affichage particulier à une autre doit avoir une transition 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 supérieure ou inférieure de la séquence.

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

Pour ce faire, vous pouvez utiliser des types de transition d'affichage, qui vous permettent d'attribuer un ou plusieurs types à une transition Active View. Par exemple, lors de la transition vers une page de niveau supérieur dans une séquence de pagination, utilisez le type forwards, et lorsque vous accédez à 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 chaque type peut être personnalisé via CSS pour utiliser différentes animations.

Pour utiliser des types dans une transition d'affichage pour 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 au sélecteur l'élément type que vous souhaitez cibler. Cela vous permet de séparer les styles de plusieurs transitions d'affichage, 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 la transition, vous pouvez utiliser le sélecteur pour définir ou désactiver 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 page auquel vous accédez. Les types sont déterminés lors du clic qui les transmet à 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.

html:active-view-transition {
    …
}

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

Parfois, la transition d'un type de vue particulier à un autre doit être spécifiquement adaptée. Une navigation « Retour » doit être différente d'une navigation « Suivant ».

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 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 dans 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 terminée. Les autres propriétés de cet objet sont abordé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 auxquels une view-transition-name est appliquée.


Exécuter des transitions sans figer les autres animations

Regardez cette démonstration d'une transition vers une 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, il est directement ralenti:

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 en direct 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 ne voyiez pas le "problème" lors de la lecture de la transition à sa vitesse normale, je ne prendrais pas la peine de la modifier.

Si vous voulez vraiment résoudre 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;
}

Voilà, c'est terminé !

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

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


Animation avec JavaScript

Jusqu'à présent, toutes les transitions ont été définies à l'aide de CSS, mais ce n'est pas toujours suffisant:

Transition de cercle. Démonstration minimale : Source

Certains aspects de cette transition ne peuvent pas être réalisés en utilisant uniquement les CSS:

  • L'animation commence à l'endroit où l'utilisateur clique.
  • L'animation se termine par le cercle dont le rayon s'étend jusqu'à l'angle le plus éloigné. Toutefois, nous espérons que cela sera possible à l'avenir avec les CSS.

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 abordées dans la documentation de référence de l'API.


Amélioration des transitions

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 passer à l'état "erreur" si le changement DOM aboutit, mais échouer. Idéalement, la transition ne devrait pas échouer, mais si elle se produit, elle ne devrait pas perturber le reste de l'expérience utilisateur.

Pour traiter les transitions comme une amélioration, veillez à ne pas utiliser les promesses de transition d'une manière qui entraînerait des problèmes dans 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 si la transition ne peut pas atteindre l'état ready, mais cela ne signifie pas que la vue n'a pas pu changer. Le DOM a peut-être é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 du DOM et pour la refuser en cas d'échec. switchView n'est plus refusé en cas d'échec de la transition. Le processus se résout une fois la mise à jour DOM terminée et le refus en cas d'échec.

Si vous souhaitez que switchView se résolve une fois que la nouvelle vue est "définie" (par exemple, si une transition animée s'est terminée ou est passée à la fin), remplacez transition.updateCallbackDone par transition.finished.


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

Cette fonctionnalité est difficile à émuler. Cependant, cette fonction d'assistance facilite grandement les choses dans les navigateurs qui ne sont pas compatibles avec les transitions d'affichage:

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 d'affichage, updateDOM est tout de même appelé, mais il n'y a pas de transition animée.

Vous pouvez également fournir des classNames à ajouter à <html> pendant la transition, ce qui permet 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 qui acceptent les transitions de vue. Cette option est utile si les utilisateurs souhaitent désactiver les transitions sur votre site.


Travailler avec des frameworks

Si vous travaillez avec une bibliothèque ou un framework qui élimine les modifications DOM, la difficulté consiste à savoir quand la modification DOM est terminée. Voici une série d'exemples utilisant l'aide ci-dessus, dans différents frameworks.

  • Réagir : la clé ici est flushSync, qui applique un ensemble de changements d'état de manière synchrone. Oui, l'utilisation de cette API fait l'objet d'un avertissement important, mais Dan Abramov m'assure que cette API 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 correct.
  • Vue.js, la clé ici est nextTick, qui est exécutée une fois le DOM mis à jour.
  • Svelte : très semblable à Vue, mais la méthode pour attendre le prochain changement est tick.
  • Lit : l'élément clé ici est la promesse this.updateComplete dans les composants, qui se produit une fois le DOM mis à jour.
  • Angular : la clé est applicationRef.tick, qui permet de supprimer les modifications DOM en attente. Depuis la version 17 d'Angular, vous pouvez utiliser les withViewTransitions fournis avec @angular/router.

Documentation de référence de l'API

const viewTransition = document.startViewTransition(update)

Lancez un nouveau ViewTransition.

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

Ensuite, lorsque la promesse renvoyée par updateCallback se termine, la transition commence dans l'image suivante. Si la promesse renvoyée par updateCallback est rejetée, la transition est abandonnée.

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

Démarrer une nouvelle ViewTransition avec les types spécifiés

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

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

Membres de l'instance de ViewTransition:

viewTransition.updateCallbackDone

Une promesse qui s'exécute lorsque la promesse renvoyée par updateCallback est remplie ou si elle est refusée si elle est refusée.

L'API View Transition encapsule une modification DOM et crée une transition. Cependant, parfois, vous ne vous souciez pas du succès ou de l'échec de l'animation de transition. Vous voulez simplement savoir si et quand la modification DOM se produit. updateCallbackDone est adapté à ce cas d'utilisation.

viewTransition.ready

Une 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 démarrer.

Elle est refusée 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 permet d'animer les pseudo-éléments de transition avec JavaScript.

viewTransition.finished

Une promesse qui s'exécute une fois que l'état final est entièrement visible et interactif pour l'utilisateur.

Elle n'accepte 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. finished est alors exécuté.

viewTransition.types

Un objet de type Set qui contient les types de la 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 transition.

Les types sont automatiquement nettoyés à la fin de la transition d'affichage.

viewTransition.skipTransition()

Ignore la partie animation de la transition.

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


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

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

Parfaitement positionné.

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

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

::view-transition-image-pair

Parfaitement positionné pour répondre aux besoins du groupe.

Dispose de isolation: isolate pour limiter l'effet du mix-blend-mode sur l'ancienne et la nouvelle vue.

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

Il est parfaitement positionné en haut à gauche du wrapper.

Remplit 100% de la largeur du groupe, mais utilise une hauteur automatique afin de conserver ses proportions au lieu de remplir le groupe.

Comporte mix-blend-mode: plus-lighter pour permettre un véritable fondu enchaîné.

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


Commentaires

Vos commentaires sont toujours importants pour les développeurs. Pour ce faire, signalez un problème au groupe de travail CSS sur GitHub en posant des suggestions et en posant des questions. Résolvez le problème avec [css-view-transitions].

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