Reibungslose und einfache Übergänge mit der View Transitions API

Jake Archibald
Jake Archibald

Unterstützte Browser

  • 111
  • 111
  • x
  • x

Quelle

Mit der View Transition API lässt sich das DOM in einem einzigen Schritt ändern. Dabei wird ein animierter Übergang zwischen den beiden Zuständen erstellt. Sie ist in Chrome 111 und höher verfügbar.

Mit der View Transition API erstellte Übergänge. Demowebsite ausprobieren – Chrome 111 oder höher erforderlich.

Wozu brauchen wir diese Funktion?

Seitenübergänge sehen nicht nur toll aus, sie geben auch die Richtung des Ablaufs an und machen deutlich, welche Elemente von Seite zu Seite im Zusammenhang stehen. Sie können sogar während des Datenabrufs auftreten, was zu einer schnelleren Wahrnehmung der Leistung führt.

Animationstools im Web sind jedoch bereits vorhanden, zum Beispiel CSS-Übergänge, CSS-Animationen oder die Web Animation API. Warum brauchen wir also etwas Neues, um Inhalte zu verschieben?

Die Wahrheit ist, dass Statusübergänge schwierig sind, selbst mit den Tools, die wir bereits haben.

Auch bei einer einfachen Überblendung sind beide Zustände gleichzeitig vorhanden. Das bringt Usability-Herausforderungen mit sich, beispielsweise den Umgang mit zusätzlichen Interaktionen bezüglich des ausgehenden Elements. Bei Benutzern von Hilfsgeräten gibt es außerdem einen Zeitraum, in dem sich der Vorher- und Nachher-Zustand gleichzeitig im DOM befindet, sodass sich die Elemente im Baum möglicherweise so bewegen, dass die Leseposition und der Fokus leicht verloren gehen.

Der Umgang mit Statusänderungen ist besonders schwierig, wenn sich die beiden Status in der Scrollposition unterscheiden. Wenn ein Element von einem Container in einen anderen verschoben wird, können Probleme mit overflow: hidden und anderen Arten von Clipping auftreten. Das bedeutet, dass Sie Ihren CSS-Code neu strukturieren müssen, um den gewünschten Effekt zu erzielen.

Es ist nicht unmöglich, sondern schwierig.

Mit Ansichtsübergängen wird es einfacher, das DOM zu ändern, ohne dass sich die Status überschneiden. Sie können aber mithilfe von Snapshot-Ansichten eine Übergangsanimation zwischen den Status erstellen.

Außerdem wird diese Funktion erweitert, um einen Wechsel zwischen vollständigen Seitenladevorgängen zu ermöglichen, was derzeit nicht möglich ist, obwohl die aktuelle Implementierung auf Single-Page-Apps (SPAs) ausgerichtet ist.

Standardisierungsstatus

Die Funktion wird in der W3C CSS Working Group als Spezifikationsentwurf entwickelt.

Sobald wir mit dem API-Design zufrieden sind, beginnen wir mit den Prozessen und Prüfungen, die erforderlich sind, um diese Funktion an den stabilen Status zu senden.

Entwicklerfeedback ist sehr wichtig. Bitte melde Probleme auf GitHub und stelle Fragen und Vorschläge zur Verfügung.

Der einfachste Übergang: ein Überblenden

Die standardmäßige Ansichtsübergänge ist eine Überblendung und dient daher als eine gute Einführung in das 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));
}

Dabei ändert updateTheDOMSomehow das DOM in den neuen Status. Sie können das ganz nach Belieben tun: Elemente hinzufügen oder entfernen, Klassennamen oder Stile ändern... es ist egal.

Und schon werden die Seiten verblasst:

Die standardmäßige Überblendung. Minimale Demo. Quelle.

Okay, ein Überblenden ist nicht so beeindruckend. Die Übergänge können zwar angepasst werden, aber bevor wir dazu kommen, müssen wir verstehen, wie diese grundlegende Überblendung funktioniert.

So funktionieren diese Umstellungen

Nehmen wir das Codebeispiel von oben:

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

Wenn .startViewTransition() aufgerufen wird, erfasst die API den aktuellen Status der Seite. Dazu gehört auch das Erstellen eines Screenshots.

Sobald dies abgeschlossen ist, wird der an .startViewTransition() übergebene Callback aufgerufen. Hier wird das DOM geändert. Anschließend erfasst die API den neuen Zustand der Seite.

Sobald der Status erfasst ist, konstruiert die API einen Pseudoelementbaum wie folgt:

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

Das ::view-transition wird in einem Overlay über dem Rest der Seite platziert. Das ist hilfreich, wenn Sie eine Hintergrundfarbe für den Übergang festlegen möchten.

::view-transition-old(root) ist ein Screenshot der alten Ansicht und ::view-transition-new(root) ist eine Live-Darstellung der neuen Ansicht. Beide werden als CSS-„ersetzter Inhalt“ gerendert (wie ein <img>).

Die alte Ansicht wird von opacity: 1 zu opacity: 0 animiert, während die neue Ansicht von opacity: 0 zu opacity: 1 animiert wird, wodurch eine Überblendung entsteht.

Die gesamte Animation wird mithilfe von CSS-Animationen durchgeführt, sodass sie mit CSS angepasst werden können.

Einfache Anpassung

Alle oben genannten Pseudoelemente können mit CSS ausgerichtet werden. Da die Animationen mithilfe von CSS definiert werden, können Sie sie mithilfe der vorhandenen CSS-Animationseigenschaften ändern. Beispiel:

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

Mit dieser einen Änderung geht das Ausblenden jetzt sehr langsam:

Lange Überblendung. Minimale Demo. Quelle.

Okay, das ist immer noch nicht beeindruckend. Implementieren wir stattdessen den gemeinsamen Achsenübergang in 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;
}

Und so sieht das Ergebnis aus:

Übergang der gemeinsamen Achse. Minimale Demo. Quelle.

Mehrere Elemente umstellen

In der vorherigen Demo wurde die gesamte Seite am gemeinsamen Achsenübergang beteiligt. Das funktioniert für den Großteil der Seite, aber für die Überschrift scheint es nicht ganz zu stimmen, da sie einfach herausgeschoben wird, um wieder hineinzuschieben.

Um dies zu vermeiden, können Sie den Header aus dem Rest der Seite extrahieren, um ihn separat zu animieren. Dazu wird dem Element ein view-transition-name zugewiesen.

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

Der Wert von view-transition-name kann beliebig sein (mit Ausnahme von none, d. h., es gibt keinen Übergangsnamen). Damit wird das Element während des Übergangs eindeutig identifiziert.

Das Ergebnis:

Gemeinsamer Achsenübergang mit fester Kopfzeile. Minimale Demo. Quelle.

Jetzt bleibt der Header an Ort und Stelle und wird überblendet.

Diese CSS-Deklaration hat die Änderung des Pseudoelement-Baums verursacht:

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

Es gibt jetzt zwei Übergangsgruppen. eine für den Header und eine für den Rest. Sie können mit CSS ein unabhängiges Targeting und mit unterschiedlichen Übergängen festlegen. Allerdings wurde für main-header in diesem Fall der Standardübergang beibehalten, der eine Überblendung darstellt.

Ok, der Standardübergang ist nicht nur ein Überblenden, der ::view-transition-group wechselt auch:

  • Positionieren und transformieren (über transform)
  • Breite
  • Größe

Das spielt bisher keine Rolle, da der Header dieselbe Größe und Position hat und sich beide Seiten des DOMs ändern. Wir können aber auch den Text in der Kopfzeile extrahieren:

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

fit-content wird verwendet, damit das Element der Größe des Texts entspricht, anstatt es auf die verbleibende Breite zu strecken. Andernfalls verkleinert der Zurückpfeil die Größe des Kopfzeilentextelements, während es auf beiden Seiten dieselbe Größe haben soll.

Jetzt müssen wir mit drei Teilen experimentieren:

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

Aber auch hier gilt wieder, nur die Standardeinstellungen zu verwenden:

Bewegender Text für die Überschrift. Minimale Demo. Quelle.

Jetzt gleitet der Überschriftentext etwas zufriedenstellend über die Seite, um Platz für die Schaltfläche „Zurück“ zu schaffen.

Fehler bei Umstellungen beheben

Da Ansichtsübergänge auf CSS-Animationen basieren, eignet sich das Steuerfeld Animationen in den Chrome-Entwicklertools hervorragend für das Debugging von Übergängen.

Im Steuerfeld Animationen können Sie die nächste Animation anhalten und dann mit Scrubbing durch die Animation streichen. Währenddessen finden Sie die Pseudoelemente des Übergangs im Steuerfeld Elemente.

Debugging View-Übergänge mit Chrome-Entwicklertools

Übergehende Elemente müssen nicht dasselbe DOM-Element sein

Bisher haben wir mit view-transition-name separate Übergangselemente für den Header und den Text im Header erstellt. Prinzipiell handelt es sich um dieselben Elemente vor und nach der DOM-Änderung, aber Sie können Übergänge erstellen, wenn dies nicht der Fall ist.

Der Haupteinbettung des Videos kann beispielsweise ein view-transition-name zugewiesen werden:

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

Wenn dann auf das Thumbnail geklickt wird, kann ihm für die Dauer des Übergangs derselbe view-transition-name zugewiesen werden:

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

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

Und das Ergebnis:

Ein Element geht in ein anderes über. Minimale Demo. Quelle.

Das Thumbnail wechselt jetzt zum Hauptbild. Obwohl sie sich konzeptionell (und buchstäblich) voneinander unterscheiden, behandelt die Transition API sie als identisch, da sie dieselbe view-transition-name haben.

Der eigentliche Code dafür ist etwas komplizierter als im einfachen Beispiel oben, da er auch den Übergang zurück zur Thumbnail-Seite übernimmt. Die vollständige Implementierung finden Sie in der Quelle.

Benutzerdefinierte Ein- und Exit-Übergänge

Sehen Sie sich dieses Beispiel an:

Seitenleiste aufrufen und schließen. Minimale Demo. Quelle.

Die Seitenleiste ist Teil der Umstellung:

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

Anders als die Kopfzeile im vorherigen Beispiel erscheint die Seitenleiste jedoch nicht auf allen Seiten. Wenn beide Status die Seitenleiste haben, sehen die Pseudoelemente beim Übergang so aus:

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

Wenn sich die Seitenleiste jedoch nur auf der neuen Seite befindet, ist das Pseudoelement ::view-transition-old(sidebar) nicht vorhanden. Da es für die Seitenleiste kein "altes" Bild gibt, hat das Bildpaar nur ein ::view-transition-new(sidebar). Wenn sich die Seitenleiste nur auf der alten Seite befindet, hat das Bildpaar dagegen nur ein ::view-transition-old(sidebar).

In der obigen Demo ändert sich der Übergang der Seitenleiste, je nachdem, ob sie in beiden Status aufgerufen, verlassen oder vorhanden ist. Er schiebt nach rechts und wird wieder eingeblendet, indem er nach rechts geschoben und dann wieder ausgeblendet wird. Wenn er sich in beiden Bundesstaaten befindet, bleibt er an Ort und Stelle.

Wenn Sie bestimmte Ein- und Ausstiegsübergänge erstellen möchten, können Sie die Pseudoklasse :only-child für die Ausrichtung auf das alte/neue Pseudoelement verwenden, wenn es das einzige untergeordnete Element im Bildpaar ist:

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

In diesem Fall gibt es keinen speziellen Übergang für den Fall, dass die Seitenleiste in beiden Status vorhanden ist, da die Standardeinstellung perfekt ist.

Asynchrone DOM-Aktualisierungen und Warten auf Inhalte

Der an .startViewTransition() übergebene Callback kann ein Promise zurückgeben, das asynchrone DOM-Aktualisierungen ermöglicht und darauf wartet, dass wichtige Inhalte bereit sind.

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

Die Umstellung wird erst gestartet, wenn das Versprechen erfüllt ist. Während dieser Zeit ist die Seite eingefroren, sodass die Verzögerungen hier auf ein Minimum reduziert werden sollten. Insbesondere sollten Netzwerkabrufe vor dem Aufruf von .startViewTransition() durchgeführt werden, während die Seite noch vollständig interaktiv ist, und nicht als Teil des .startViewTransition()-Callbacks.

Wenn Sie warten möchten, bis Bilder oder Schriftarten bereit sind, verwenden Sie ein aggressives Zeitlimit:

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

In manchen Fällen ist es jedoch besser, die Verzögerung vollständig zu vermeiden und die bereits vorhandenen Inhalte zu verwenden.

Bereits vorhandene Inhalte optimal nutzen

Wenn die Miniaturansicht zu einem größeren Bild übergeht:

Die Standardeinstellung ist „Überblenden“. Das bedeutet, dass das Thumbnail möglicherweise mit einem noch nicht geladenen vollständigen Bild überblendet.

Eine Möglichkeit besteht darin, mit dem Übergang zu warten, bis das vollständige Bild geladen ist. Idealerweise sollte dies vor dem Aufruf von .startViewTransition() erfolgen, damit die Seite interaktiv bleibt und ein rotierendes Ladesymbol eingeblendet wird, um den Nutzer darüber zu informieren, dass Inhalte geladen werden. Aber in diesem Fall gibt es eine bessere Methode:

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

Das Thumbnail verschwindet jetzt nicht, sondern befindet sich einfach unter dem Vollbild. Wenn also die neue Ansicht nicht geladen wurde, ist das Thumbnail während des Übergangs sichtbar. Der Übergang kann also sofort beginnen und das vollständige Bild kann in einem eigenen Tempo geladen werden.

Dies würde nicht funktionieren, wenn die neue Ansicht Transparenz aufweisen würde, aber in diesem Fall wissen wir, dass das nicht der Fall ist, sodass wir diese Optimierung vornehmen können.

Umgang mit Änderungen des Seitenverhältnisses

Bisher erfolgten alle Übergänge auf Elemente mit demselben Seitenverhältnis. Das ist jedoch nicht immer der Fall. Was ist, wenn das Thumbnail 1:1 und das Hauptbild 16:9 ist?

Ein Element geht in ein anderes über, wobei sich das Seitenverhältnis ändert. Minimale Demo. Quelle.

Beim Standardübergang wird die Gruppe von der Vorher- zur Nachher-Größe animiert. Die alte und die neue Ansicht haben eine Breite von 100% mit der automatischen Höhe. Das bedeutet, dass ihr Seitenverhältnis unabhängig von der Größe der Gruppe beibehalten wird.

Das ist eine gute Standardeinstellung, aber in diesem Fall ist es nicht das, was wir wollen. Das bedeutet:

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

Das bedeutet, dass die Miniaturansicht bei Vergrößerung der Breite in der Mitte des Elements bleibt, das gesamte Bild jedoch beim Übergang von 1:1 zu 16:9 nicht zugeschnitten wird.

Übergang je nach Gerätestatus ändern

Sie können auf Mobilgeräten unterschiedliche Übergänge im Vergleich zu Computern verwenden. In diesem Beispiel wird auf Mobilgeräten eine vollständige Folie von der Seite gezeigt, auf Desktop jedoch eine subtilere Folie:

Ein Element geht in ein anderes über. Minimale Demo. Quelle.

Dies kann mit normalen Medienabfragen erreicht werden:

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

Abhängig von den passenden Medienabfragen können Sie auch ändern, welchen Elementen Sie eine view-transition-name zuweisen.

Reaktion auf die Einstellung für weniger Bewegung

Nutzer können über ihr Betriebssystem angeben, dass sie reduzierte Bewegungselemente bevorzugen. Diese Präferenz wird über CSS angezeigt.

Sie können die Umstellung für folgende Nutzer verhindern:

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

Die Einstellung für „Reduzierte Bewegung“ bedeutet jedoch nicht, dass der Nutzer keine Bewegungen wünscht. Anstelle der obigen Ausführung können Sie auch eine subtilere Animation wählen, die dennoch die Beziehung zwischen den Elementen und dem Datenfluss zum Ausdruck bringt.

Ändern des Übergangs je nach Art der Navigation

Manchmal sollte die Navigation von einem bestimmten Seitentyp zu einem anderen speziell angepasst werden. Alternativ sollte sich eine Navigation "Zurück" von einer Vorwärtsnavigation unterscheiden.

Andere Übergänge beim Zurückkehren. Minimale Demo. Quelle.

In solchen Fällen empfiehlt es sich, für <html> einen Klassennamen festzulegen, der auch als Dokumentelement bezeichnet wird:

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

In diesem Beispiel wird transition.finished verwendet, ein Promise, das aufgelöst wird, sobald der Übergang sein Endzustand erreicht hat. Weitere Eigenschaften dieses Objekts finden Sie in der API-Referenz.

Jetzt können Sie diesen Klassennamen in Ihrem CSS-Code verwenden, um die Umstellung zu ändern:

/* '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;
}

Wie bei Medienabfragen kann das Vorhandensein dieser Klassen auch dazu verwendet werden, zu ändern, welche Elemente ein view-transition-name erhalten.

Übergang ohne Einfrieren anderer Animationen

Sehen Sie sich diese Demo einer Videoübergangsposition an:

Videoübergang. Minimale Demo. Quelle.

Ist dir etwas ausgesprochen? Keine Sorge, wenn nicht. Hier wird es verlangsamt:

Videoübergang, langsamer. Minimale Demo. Quelle.

Während des Übergangs erscheint das Video scheinbar eingefroren. Anschließend wird die wiedergegebene Version eingeblendet. Das liegt daran, dass ::view-transition-old(video) ein Screenshot der alten Ansicht ist, während ::view-transition-new(video) ein Live-Bild der neuen Ansicht ist.

Du kannst dieses Problem beheben, aber frage dich zuerst, ob es sich lohnt, es zu beheben. Sollte das "Problem" bei der Wiedergabe mit normaler Geschwindigkeit nicht auftreten, würde ich es nicht ändern.

Wenn Sie das Problem wirklich beheben möchten, sollten Sie die ::view-transition-old(video) nicht einblenden. Wechseln Sie stattdessen direkt zu ::view-transition-new(video). Dazu können Sie die Standardstile und -animationen überschreiben:

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

Webseite.

Videoübergang, langsamer. Minimale Demo. Quelle.

Jetzt wird das Video während des Übergangs abgespielt.

Animationen mit JavaScript

Bisher wurden alle Übergänge mit CSS definiert, aber manchmal ist CSS nicht ausreichend:

Kreisübergang. Minimale Demo. Quelle.

Einige Teile dieser Umstellung sind mit CSS allein nicht möglich:

  • Die Animation beginnt an der Klickposition.
  • Die Animation endet mit dem Kreis mit einem Radius zur entferntesten Ecke. Wir hoffen, dass dies in Zukunft mit Preisvergleichsportalen möglich sein wird.

Übergänge können Sie glücklicherweise mit der Web Animation API erstellen.

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)',
      }
    );
  });
}

In diesem Beispiel wird transition.ready verwendet, ein Promise, das aufgelöst wird, nachdem die Pseudoelemente des Übergangs erstellt wurden. Weitere Eigenschaften dieses Objekts finden Sie in der API-Referenz.

Übergänge als Verbesserung

Die View Transition API wurde entwickelt, um eine DOM-Änderung zu „umfassen“ und einen Übergang dafür zu erstellen. Der Übergang sollte jedoch als Verbesserung behandelt werden, da die Anwendung nicht in den Status „Fehler“ wechseln sollte, wenn die DOM-Änderung erfolgreich ist, die Umstellung jedoch fehlschlägt. Im Idealfall sollte die Umstellung nicht fehlschlagen. Falls doch, sollte der Rest der Nutzererfahrung nicht beeinträchtigt werden.

Übergänge werden als Verbesserung behandelt. Verwende daher keine Übergangsversprechen, die dazu führen könnten, dass deine App ausgelöst wird, wenn der Übergang fehlschlägt.

Don'ts
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)',
    }
  );
}

Das Problem bei diesem Beispiel ist, dass switchView() abgelehnt wird, wenn der Übergang nicht den Status ready erreichen kann. Das bedeutet aber nicht, dass die Ansicht nicht gewechselt werden konnte. Das DOM wurde möglicherweise aktualisiert, aber es gab doppelte view-transition-names, sodass der Übergang übersprungen wurde.

Gehen Sie in diesem Fall so vor:

Das sollten Sie tun:
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
  }
}

In diesem Beispiel wird transition.updateCallbackDone verwendet, um auf die DOM-Aktualisierung zu warten und sie abzulehnen, wenn sie fehlschlägt. switchView lehnt den Wechsel nicht mehr ab, wenn der Übergang fehlschlägt, wird er aufgelöst, wenn die DOM-Aktualisierung abgeschlossen ist, und lehnt ihn ab, wenn er fehlschlägt.

Soll switchView aufgelöst werden, sobald die neue Ansicht beglichen ist (z. B. wenn ein animierter Übergang abgeschlossen oder bis zum Ende übersprungen wurde), ersetzen Sie transition.updateCallbackDone durch transition.finished.

Kein Polyfill, aber...

Ich glaube nicht, dass diese Funktion auf irgendeine Weise mit Polypen gefüllt werden kann, aber ich freue mich, dass dies falsch ist.

Diese Hilfsfunktion erleichtert jedoch die Arbeit in Browsern, die keine Ansichtsübergänge nicht unterstützen, viel einfacher:

function transitionHelper({
  skipTransition = false,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

Und es kann so verwendet werden:

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

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

In Browsern, die Ansichtsübergänge nicht unterstützen, wird updateDOM zwar aufgerufen, es erfolgt jedoch kein animierter Übergang.

Sie können auch einige classNames angeben, die während der Umstellung zu <html> hinzugefügt werden sollen. So lassen sich den Übergang je nach Art der Navigation leichter ändern.

Wenn keine Animation angezeigt werden soll, kannst du auch true an skipTransition übergeben. Das funktioniert sogar in Browsern, die View Übergänge unterstützen. Das ist nützlich, wenn Nutzer für Ihre Website die Umstellung deaktivieren möchten.

Mit Frameworks arbeiten

Wenn Sie mit einer Bibliothek oder einem Framework arbeiten, das DOM-Änderungen abstrahiert, ist es schwierig zu wissen, wann die DOM-Änderung abgeschlossen ist. Im Folgenden finden Sie eine Reihe von Beispielen, die anhand des oben beschriebenen Helpers in verschiedenen Frameworks verwendet werden.

  • Reagieren: Der Schlüssel ist hier flushSync, mit dem eine Reihe von Statusänderungen synchron angewendet wird. Ja, es gibt eine große Warnung bezüglich der Verwendung dieser API, aber Dan Abramov versichert mir, dass dies in diesem Fall angemessen ist. Wie bei React und asynchronem Code solltest du bei Verwendung der verschiedenen Promise, die von startViewTransition zurückgegeben werden, darauf achten, dass dein Code mit dem richtigen Status ausgeführt wird.
  • Vue.js: Der Schlüssel ist hier nextTick. Er wird ausgeführt, sobald das DOM aktualisiert wurde.
  • Svelte – sehr ähnlich wie Vue, aber die Methode, mit der die nächste Änderung erwartet wird, ist tick.
  • Lit: Das Entscheidende ist hier das Versprechen this.updateComplete in den Komponenten, das erfüllt wird, sobald das DOM aktualisiert wurde.
  • Angular: Der Schlüssel ist hier applicationRef.tick. Damit werden ausstehende DOM-Änderungen gelöscht. Ab Angular Version 17 können Sie withViewTransitions verwenden, die in @angular/router enthalten ist.

API-Referenz

const viewTransition = document.startViewTransition(updateCallback)

Starte einen neuen ViewTransition.

updateCallback wird aufgerufen, sobald der aktuelle Status des Dokuments erfasst wurde.

Wenn dann das von updateCallback zurückgegebene Versprechen erfüllt, beginnt der Übergang im nächsten Frame. Wenn das von updateCallback zurückgegebene Promise abgelehnt wird, wird die Umstellung abgebrochen.

Instanzmitglieder von ViewTransition:

viewTransition.updateCallbackDone

Ein Versprechen, das erfüllt wird, wenn das von updateCallback zurückgegebene Versprechen erfüllt bzw. abgelehnt wird.

Die View Transition API umschließt eine DOM-Änderung und erstellt einen Übergang. Manchmal ist Ihnen jedoch der Erfolg oder Misserfolg der Übergangsanimation nicht wichtig, Sie möchten nur wissen, ob und wann die DOM-Änderung erfolgt. updateCallbackDone ist für diesen Anwendungsfall gedacht.

viewTransition.ready

Ein Versprechen, das erfüllt wird, sobald die Pseudoelemente für den Übergang erstellt sind und die Animation gleich beginnt.

Wird abgelehnt, wenn die Umstellung nicht beginnen kann. Das kann an einer fehlerhaften Konfiguration liegen, z. B. doppelte view-transition-names oder wenn updateCallback ein abgelehntes Promise zurückgibt.

Dies ist nützlich, wenn Sie die Übergangs-Pseudoelemente mit JavaScript animieren möchten.

viewTransition.finished

Ein Versprechen, das erfüllt wird, sobald der Endzustand für den Nutzer vollständig sichtbar und interaktiv ist.

Es wird nur abgelehnt, wenn updateCallback ein abgelehntes Promise zurückgibt, was darauf hinweist, dass der Endstatus nicht erstellt wurde.

Wenn ein Übergang nicht beginnt oder während des Übergangs übersprungen wird, ist der Endstatus dennoch erreicht, sodass finished erfüllt ist.

viewTransition.skipTransition()

Die Animation des Übergangs überspringen.

Dadurch wird der Aufruf von updateCallback nicht übersprungen, da die DOM-Änderung von der Umstellung getrennt ist.

Standardstil und -übergangsreferenz

::view-transition
Das Pseudoelement, das den Darstellungsbereich ausfüllt und jedes ::view-transition-group-Element enthält.
::view-transition-group

Perfekte Position.

Übergänge width und height zwischen den Status „Vorher“ und „Nachher“.

Übergänge transform zwischen dem „Vorher“- und „Nachher“-Quadrat im Darstellungsbereich.

::view-transition-image-pair

Absolut positioniert, um die Gruppe zu füllen.

Hat isolation: isolate, um die Auswirkungen des Mischmodus plus-lighter auf die alte und die neue Ansicht einzuschränken.

::view-transition-new und ::view-transition-old

Sie ist absolut links oben im Wrapper positioniert.

Füllt 100% der Gruppenbreite aus, hat aber eine automatische Höhe. Dadurch wird das Seitenverhältnis beibehalten, die Gruppe wird also nicht ausgefüllt.

Verfügt über mix-blend-mode: plus-lighter, um ein echtes Überblenden zu ermöglichen.

Die alte Ansicht wechselt von opacity: 1 zu opacity: 0. Die neue Ansicht wechselt von opacity: 0 zu opacity: 1.

Feedback

Entwicklerfeedback ist in dieser Phase sehr wichtig. Bitte melde Probleme auf GitHub und stelle Fragen und Vorschläge bereit.