Startseite / Artikel

Richtungsabhängige Ansichtsübergänge in React mit addTransitionType

Erfahren Sie, wie man mit addTransitionType Tags zu einer React-Übergangsanimation hinzufügt, sodass ViewTransition, CSS und Ereignishandler eine Schieberichtung ohne zusätzlichen Zustand auswählen können.

Verlag: REACTAPP.TOP13 Min. Lesezeit2900 Wörter

Quellen: addTransitionType in React 19.3: Direction-Aware Animations Without Direction State

Stellen Sie sich ein mehrstufiges Bestellverfahren mit „Weiter“- und „Zurück“-Schaltflächen vor. Beide aktualisieren den gleichen step-Wert, doch die Animationen sollten unterschiedlich sein: Beim Vorwärtsgehen soll das neue Panel von rechts eingeblendet werden, beim Zurückgehen von links. Da sich der Zustandswechsel in beiden Fällen identisch darstellt, verfügt das Komponente nicht über eine eingebaute Möglichkeit, herauszufinden, welche Animation abgespielt werden soll. Die klassische Lösung ist ein separater direction-Wert, von dem man hofft, dass er zum Zeitpunkt der Animation noch korrekt ist.

React 19.3 fügt addTransitionType hinzu, um dieses Problem sauber zu lösen. Sie rufen diese Funktion innerhalb von startTransition auf, um den Grund für eine Aktualisierung zu erfassen, und <ViewTransition> verwendet diesen Grund, um eine Animation auszuwählen. Nach dem Lesen dieses Textes wissen Sie, an welchen drei Stellen ein Transition Type verwendet werden kann, wie sich die Typen kombinieren, auf welche Fallstricke Sie achten müssen und wie Sie denselben Effekt in älteren React-Versionen erzielen können. Es baut auf der <ViewTransition>-Komponente selbst auf; falls diese Komponente neu für Sie ist, beginnen Sie zunächst mit der entsprechenden React-Dokumentation.

Die gesamte API in zwei Imports

Die Funktion befindet sich im Haupt-react-Paket neben startTransition.

import { startTransition, addTransitionType } from 'react';

Jeder Navigation-Handler öffnet eine Transition, beschriftet sie mit einem String und führt anschließend die übliche Zustandsaktualisierung durch. Die Beschriftung kann jeder gewünschte String sein; hier verwenden die beiden Handler next und back.

function goNext() {
  startTransition(() => {
    addTransitionType('next');   // any string
    setStep(s => s + 1);
  });
}function goBack() {
  startTransition(() => {
    addTransitionType('back');
    setStep(s => s - 1);
  });
}

Der Vertrag ist einfach. addTransitionType(type) nimmt einen einzigen String entgegen und hat keinen Rückgabewert. Er muss innerhalb des Callbacks ausgeführt werden, der an startTransition übergeben wird. Wenn man ihn an einer anderen Stelle aufruft, protokolliert die Entwicklerversion von React die Warnung „addTransitionType kann nur innerhalb eines startTransition() Callbacks aufgerufen werden“ und ignoriert den Typ. Mehrfache Aufrufe sind zulässig, und React sammelt alle Typen in derselben Transition zusammen.

Der interessante Teil ist nicht die Funktion selbst, sondern ihre Aufrufer. Es gibt drei davon.

Verbraucher 1: Objektwerte in den ViewTransition-Props

Jeder Auslöser-Parameter von <ViewTransition> (enter, exit, update, share und default) kann ein Objekt statt nur einen einfachen Klassennamen entgegennehmen. Dieses Objekt ordnet Übergangstypen einem Klassennamen, "auto" oder "none" zu. Der Schlüssel default dient als Ersatz, wenn keiner der aufgelisteten Typen vorhanden ist.

<ViewTransition
  enter={{ next: 'from-right', back: 'from-left', default: 'auto' }}
  exit={{ next: 'to-left', back: 'to-right', default: 'auto' }}
>
  <Step />
</ViewTransition>

Ein vollständiger Assistent, der von Typen gesteuert wird

Der folgende Assistent setzt dies in die Praxis um. Beachten Sie den Wert key={step} an der Grenze: Jeder Schrittwechsel entmontiert das alte Panel und montiert ein neues, wodurch React dies als Ausgangs- und Eingriffsvorgang behandelt, wobei der aktive Typ die Richtung beider Teile bestimmt.

import { ViewTransition, useState, startTransition, addTransitionType } from 'react';

Ein einziger go-Hilfsfunktion erhält den Schrittunterschied sowie den Klassennamen, sodass beide Schaltflächen denselben Codepfad teilen.

const STEPS = ['Cart', 'Address', 'Payment', 'Review'];export default function App() {
  const [step, setStep] = useState(0);  function go(delta, type) {
    startTransition(() => {
      addTransitionType(type);
      setStep(s => s + delta);
    });
  }  return (
    <>
      <div className="bar">
        <button disabled={step === 0} onClick={() => go(-1, 'back')}>← Back</button>
        <span>Step {step + 1} of {STEPS.length}</span>
        <button disabled={step === STEPS.length - 1} onClick={() => go(1, 'next')}>Next →</button>
      </div>      <div className="stage">
        <ViewTransition
          key={step}
          enter={{ next: 'from-right', back: 'from-left' }}
          exit={{ next: 'to-left', back: 'to-right' }}
        >
          <section className="panel">{STEPS[step]}</section>
        </ViewTransition>
      </div>
    </>
  );
}

Die Layout-Stile positionieren das Panel absolut innerhalb einer begrenzten Bühne, wodurch die ein- und ausgehenden Schnappschüsse sich nicht gegenseitig verschieben.

.bar { display: flex; align-items: center; gap: 12px; }
.stage { position: relative; height: 90px; margin-top: 12px; overflow: hidden; border-radius: 12px; }
.panel {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  font: 600 22px system-ui;
  background: light-dark(#e0e7ff, #1e1b4b);
}

Die Animationen richten sich über Klassen auf die Pseudo-Elemente der Ansichtsübergänge. Alte Schnappschüsse erhalten die Ausgangs-Keyframes to-left oder to-right, neue Schnappschüsse erhalten from-right oder from-left, und die vier Keyframes verschieben sich einfach über die volle Breite, während sie verblasen.

::view-transition-old(.to-left)    { animation: 260ms ease-in  both to-left; }
::view-transition-old(.to-right)   { animation: 260ms ease-in  both to-right; }
::view-transition-new(.from-right) { animation: 300ms ease-out both from-right; }
::view-transition-new(.from-left)  { animation: 300ms ease-out both from-left; }@keyframes to-left    { to   { transform: translateX(-100%); opacity: 0; } }
@keyframes to-right   { to   { transform: translateX(100%);  opacity: 0; } }
@keyframes from-right { from { transform: translateX(100%);  opacity: 0; } }
@keyframes from-left  { from { transform: translateX(-100%); opacity: 0; } }

Betrachten Sie, was der Komponente fehlt: Es gibt überhaupt keine gespeicherte Richtung. Die Richtung gehört zur Aktualisierung, wird im Moment festgelegt, in dem die Aktualisierung geplant wird, und bewegt sich zusammen mit ihr durch den React-Scheduler. Wenn ein Benutzer zweimal schnell auf „Zurück“ klickt, während der erste Slide noch abgespielt wird, gruppiert React die in der Warteschlange befindlichen Aufgaben – die zweite Animation liest anschließend die für diese zweite Transition gesammelten Typen. Es existiert kein directionRef, der aus dem Takt geraten könnte.

Wie React übereinstimmende Typen löst

React wendet diese Regeln in dieser Reihenfolge an:

  • Falls mehr als ein Typ übereinstimmt, werden die entsprechenden Klassennamen zusammengefügt und alle angewendet.
  • Falls nichts übereinstimmt, greift React auf den Eintrag default zurück; ohne diesen wird die entsprechende Animation nicht ausgeführt.
  • Wenn der zugehörige Eintrag "none" ist, hat dieser Vorrang und die Übergangsanimation wird vollständig übersprungen.
  • Consumer 2: native Ansichtsübergangstypen in CSS

    React übermittelt außerdem jeden Übergangstyp an den Browser als nativen Ansichtsübergangstyp. Dadurch können Sie Animationen aus einer Stylesheet mithilfe der Pseudo-Klasse :active-view-transition-type() einschränken, ohne die Komponenteneigenschaften zu verändern.

    Der erste Schritt besteht darin, dem Element eine view-transition-class zuzuweisen. Der Snapshot erbt diese Klasse, und React überschreibt sie nicht.

    /* give the element a class the snapshot inherits — React leaves it alone */
    .panel { view-transition-class: panel; }
    

    Legen Sie anschließend Regeln fest, die nur dann gelten, wenn ein Übergang mit einem bestimmten Typ läuft. Die verschachtelte Syntax hält die beiden Pseudo-Elemente für eine Richtung zusammen.

    /* applies only while a transition tagged 'next' is running */
    :root:active-view-transition-type(next) {
      &::view-transition-old(.panel) { animation: 220ms ease-in both to-left; }
      &::view-transition-new(.panel) { animation: 280ms ease-out both from-right; }
    }
    :root:active-view-transition-type(back) {
      &::view-transition-old(.panel) { animation: 220ms ease-in both to-right; }
      &::view-transition-new(.panel) { animation: 280ms ease-out both from-left; }
    }
    

    Derselbe Assistent, nur mit CSS gestylt

    Diese Version des Assistenten übermittelt keinerlei enter- oder exit-Eigenschaften. Die Grenze ist unverändert, und die Stylesheet trifft alle Entscheidungen basierend auf dem aktiven Typ.

    import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
    

    Der Komponentencodex ist ansonsten identisch mit dem ersten Demo.

    const STEPS = ['Cart', 'Address', 'Payment', 'Review'];export default function App() {
      const [step, setStep] = useState(0);  function go(delta, type) {
        startTransition(() => {
          addTransitionType(type);
          setStep(s => s + delta);
        });
      }  return (
        <>
          <div className="bar">
            <button disabled={step === 0} onClick={() => go(-1, 'back')}>← Back</button>
            <span>Step {step + 1} of {STEPS.length}</span>
            <button disabled={step === STEPS.length - 1} onClick={() => go(1, 'next')}>Next →</button>
          </div>
          <div className="stage">
            <ViewTransition key={step}>
              <section className="panel">{STEPS[step]}</section>
            </ViewTransition>
          </div>
        </>
      );
    }
    

    Die Panel-Stile enthalten nun view-transition-class: panel, worauf die unten stehenden Selektoren verweisen.

    .bar { display: flex; align-items: center; gap: 12px; }
    .stage { position: relative; height: 90px; margin-top: 12px; overflow: hidden; border-radius: 12px; }
    .panel {
      position: absolute; inset: 0;
      display: grid; place-items: center;
      font: 600 22px system-ui;
      background: light-dark(#fce7f3, #4a1d3a);
      view-transition-class: panel;
    }
    

    Die typengebundenen Regeln befinden sich auf :root, und die Keyframes sind unverändert geblieben.

    /* The type React forwarded to the browser scopes the whole rule */
    :root:active-view-transition-type(next) {
      &::view-transition-old(.panel) { animation: 260ms ease-in  both to-left; }
      &::view-transition-new(.panel) { animation: 300ms ease-out both from-right; }
    }
    :root:active-view-transition-type(back) {
      &::view-transition-old(.panel) { animation: 260ms ease-in  both to-right; }
      &::view-transition-new(.panel) { animation: 300ms ease-out both from-left; }
    }@keyframes to-left    { to   { transform: translateX(-100%); opacity: 0; } }
    @keyframes to-right   { to   { transform: translateX(100%);  opacity: 0; } }
    @keyframes from-right { from { transform: translateX(100%);  opacity: 0; } }
    @keyframes from-left  { from { transform: translateX(-100%); opacity: 0; } }
    

    Wählen Sie diesen Stil, wenn eine ganze Seite auf einen bestimmten Auslöser reagieren soll – beispielsweise ein Router, der Navigierungen mit navigation-forward oder navigation-back kennzeichnet. Verwenden Sie lieber Objekt-Eigenschaften, wenn eine bestimmte Grenze ihre eigene Zuordnung benötigt. Behalten Sie die Zeile view-transition-class bei: Wenn Sie (*) anstelle von (.panel) angeben, gilt die Regel auch für das Wurzelsnippet, und die ganze Seite gleitet zusammen mit dem Panel.

    Consumer 3: Event-Callback-Funktionen

    Die Callbacks onEnter, onExit, onUpdate und onShare erhalten die gesammelten Typen als ihren zweiten Parameter. Dadurch kann auch der imperative Animationsscode je nach Ursache unterschiedlich handeln. In diesem Beispiel beginnt das einsteigende Element bei back außerhalb des Bildschirms links und ansonsten rechts, wird mit der Web Animations API animiert und gibt eine Aufräumfunktion zurück, die die Animation stört, falls React sie unterbricht.

    <ViewTransition
      onEnter={(instance, types) => {
        const from = types.includes('back') ? '-100%' : '100%';
        const anim = instance.new.animate(
          [{ transform: `translateX(${from})` }, { transform: 'none' }],
          { duration: 300, easing: 'ease-out' }
        );
        return () => anim.cancel();
      }}
    >
    

    Es ist wichtig, die Aufräumfunktion zurückzugeben: Ohne sie könnte eine schnell unterbrochene Übergangsanimation eine weiterhin laufende Animation an ein nicht mehr existierendes Snapshot binden.

    Mehrere Typen auf einer Transition anwenden

    Weil React jede hinzugefügte Art innerhalb der Transition speichert, kann man eine weitere Eigenschaft auf die Richtung aufbauen. In diesem Carousel fügt ein „Quick“-Kasten ein zweites Element mit dem Namen quick hinzu. Seine Klasse verkürzt lediglich die Dauer, und beide Klassen landen gemeinsam im Snapshot.

    import { ViewTransition, useState, startTransition, addTransitionType } from 'react';
    

    Der go-Hilfsfunktion fügt immer die Richtung hinzu und fügt bedingungsweise quick hinzu. Jedes Prop-Objekt verknüpft quick mit einer Klassennamen desselben Namens, sodass eine passende Richtungsklasse und die quick-Klasse miteinander verbunden werden.

    const SLIDES = ['🍎', '🍋', '🥝', '🍇'];export default function App() {
      const [i, setI] = useState(0);
      const [quick, setQuick] = useState(false);  function go(delta, type) {
        startTransition(() => {
          addTransitionType(type);
          if (quick) addTransitionType('quick');
          setI(n => (n + delta + SLIDES.length) % SLIDES.length);
        });
      }  return (
        <>
          <div className="bar">
            <button onClick={() => go(-1, 'back')}>←</button>
            <button onClick={() => go(1, 'next')}>→</button>
            <label><input type="checkbox" checked={quick} onChange={e => setQuick(e.target.checked)} /> quick</label>
          </div>
          <div className="stage">
            <ViewTransition
              key={i}
              enter={{ next: 'from-right', back: 'from-left', quick: 'quick' }}
              exit={{ next: 'to-left', back: 'to-right', quick: 'quick' }}
            >
              <div className="slide">{SLIDES[i]}</div>
            </ViewTransition>
          </div>
        </>
      );
    }
    

    Die Anordnung folgt demselben Prinzip wie zuvor, mit einer höheren Ebene für das große Emoji.

    .bar { display: flex; align-items: center; gap: 12px; }
    .stage { position: relative; height: 110px; margin-top: 12px; overflow: hidden; border-radius: 12px; }
    .slide { position: absolute; inset: 0; display: grid; place-items: center; font-size: 56px; background: light-dark(#fef9c3, #3f3a12); }
    

    Die Richtungsregeln legen eine absichtlich langsame Animation von 700 ms fest, und die quick-Regel überschreibt nur animation-duration, wodurch die Keyframes und das Easing unverändert bleiben.

    ::view-transition-old(.to-left)    { animation: 700ms ease-in  both to-left; }
    ::view-transition-old(.to-right)   { animation: 700ms ease-in  both to-right; }
    ::view-transition-new(.from-right) { animation: 700ms ease-out both from-right; }
    ::view-transition-new(.from-left)  { animation: 700ms ease-out both from-left; }/* second class on the same snapshot: only the duration changes */
    ::view-transition-old(.quick), ::view-transition-new(.quick) { animation-duration: 120ms; }@keyframes to-left    { to   { transform: translateX(-100%); opacity: 0; } }
    @keyframes to-right   { to   { transform: translateX(100%);  opacity: 0; } }
    @keyframes from-right { from { transform: translateX(100%);  opacity: 0; } }
    @keyframes from-left  { from { transform: translateX(-100%); opacity: 0; } }
    

    Diese Komposition ist der eigentliche Vorteil des Typsystems: Orthogonale Aspekte wie Richtung, Geschwindigkeit oder Betonung können als separate Bezeichnungen ausgedrückt werden, anstatt eine wachsende Matrix aus kombinierten Klassennamen zu benötigen.

    Der ersetzte state-basierte Ansatz

    Vor dieser API befand sich die Richtung in der Regel im Zustand des Komponenten, was der falsche Ort für sie ist. Die typische Konfiguration fügt einen zweiten Zustandswert hinzu:

    const [step, setStep] = useState(0);
    const [direction, setDirection] = useState('next');
    

    Jeder Handler schreibt zunächst die Richtung und anschließend den Schritt ein, wobei die Render-Ausgabe die Richtung in einen Klassennamen umwandelt:

    function goNext() {
      setDirection('next');
      setStep(s => s + 1);
    }// then, in render:
    <div className={`panel enter-${direction}`} key={step}>…</div>
    

    Dieses Muster weist drei Schwächen auf:

    • Der zusätzliche Zustand löst eine Neurenderung für Informationen aus, die eigentlich nicht zur UI gehören.
    • Zwei schnelle Klicks in entgegengesetzten Richtungen können dazu führen, dass direction eine andere Aktualisierung beschreibt als die, die animiert wird.
  • Wenn sich der step aus einem anderen Grund ändert, wie beispielsweise durch einen Deep Link oder eine Neustart-Taste, wird die verbleibende Richtung auf ein Update angewendet, das damit nichts zu tun hat.
  • Animationsbibliotheken neigen dazu, dasselbe Problem hinter einem Prop zu verbergen. Der Wert custom={direction}, der an Varianten übergeben wird, bleibt weiterhin ein gespeicherter Wert – nur mit einem besseren Namen.

    Warum es wichtig ist, die Ursache auf das Update zu verlagern

    Mit addTransitionType ist die Ursache Teil der Aktualisierung und nicht Teil des Zustands. Der Unterschied ist gering, doch die Folgen sind erheblich: Die Ursache kann nicht veralten, dringt nicht in unverwandte Aktualisierungen ein und erfordert keine zusätzliche Renderung. Zudem schafft es einen gemeinsamen Kanal für Bibliotheken. Ein Router kann Vor- und Rücknavigationen einmal kennzeichnen, und jeder <ViewTransition> auf einer beliebigen Seite kann auf diese Kennzeichnungen reagieren. In der React-Dokumentation heißt es, dass erwartet wird, dass Router und andere Bibliotheken auf gemeinsame Typennamen hinarbeiten.

    Auch das React-Team hat erklärt, dass Typen nicht nur auf Animationen beschränkt sind und dass weitere Verwendungsmöglichkeiten für die Ursache einer Übergangssituation geplant sind. Der zugrundeliegende Mechanismus, Metadaten an einen Übergang anzuhängen und sie zum Speichern zu lesen, ist ausreichend allgemein, um dies zu unterstützen.

    Häufige Fehlerquellen, die vor der Veröffentlichung bekannt sein sollten

    • Die Typangabe ist nur innerhalb von startTransition relevant. Außerhalb davon ignoriert React die Typangabe und gibt eine Warnung aus, die nur in der Entwicklungssicht angezeigt wird; da die Aktualisierung dringend ist, animiert sich ohnehin nichts.
    • Die Typangaben werden nach jedem Commit gelöscht. Wenn die Übergangsanzeige zunächst einen <Suspense>-Fallback anzeigt, enthält dieser Fallback-Commit Ihre Typangaben, während die spätere Anzeige des eigentlichen Inhalts diese nicht enthält. Für richtungsbezogene Animationen über eine Ladegrenze sollten Sie die Typangabe erneut hinzufügen, sobald der Inhalt bereit ist, oder den Wechsel vom Fallback zum Inhalt so gestalten, dass er nicht von der Richtung abhängt.
    • "none" hat immer Vorrang. Wenn eine passende Eintragung auf "none" zurückgeführt wird, wird dieser Auslöser deaktiviert – auch dann, wenn eine andere passende Typangabe eine Klasse bereitgestellt hat.
  • Ein Objekt ohne Schlüssel default animiert nur für die von ihm aufgelisteten Typen, sodass unmarkierte Übergänge, die diese Grenze überschreiten, nicht animiert werden. Das ist oft beabsichtigt, wird aber leicht übersehen.
  • Die Browserunterstützung unterscheidet sich zwischen den beiden Ansätzen. Da React die Klasse selbstständig im Snapshot setzt, funktioniert der Objekt-Prop-Ansatz in jedem Browser mit View Transition Classes: Chrome 125, Safari 18.2 und Firefox 144. Das Styling über :active-view-transition-type() erfordert Chrome 125, Safari 18.2 oder Firefox 147. Für aktuelle Informationen sollten Sie die Kompatibilitätsdaten von MDN konsultieren.
  • Typnamen sind globale Zeichenketten, die unverändert beim Browser ankommen. Verwenden Sie Präfixe wie wizard-next, falls zwei Funktionen auf derselben Seite beide next verwenden könnten.
  • Eine Alternative für React 19.2 und frühere Versionen

    Ältere React-Versionen können addTransitionType nicht verwenden, doch die Browser-API kann den Übergangstyp weiterhin übertragen. Sie geben die Koordination des Übergangs durch React auf, nicht jedoch die Richtungsinformationen. Rufen Sie selbst document.startViewTransition auf, übergeben Sie types und wenden Sie die CSS-Regeln aus dem zweiten Verbraucher unverändert erneut an. Sie benötigen flushSync, damit die Zustandsaktualisierung synchron innerhalb des Update-Callbacks des Browsers angewendet wird:

    import { flushSync } from 'react-dom';
    

    Der Hilfsfunktion gelingt außerdem ein sanfter Rückfall: Wenn der Browser startViewTransition nicht bietet, aktualisiert er den Zustand einfach ohne Animation.

    function go(delta, type) {
      if (!document.startViewTransition) {
        setStep(s => s + delta);
        return;
      }
      document.startViewTransition({
        update: () => flushSync(() => setStep(s => s + delta)),
        types: [type],
      });
    }
    

    Nach dem Upgrade auf 19.3 verkürzt sich der Inhalt von go wieder auf die kurze startTransition-Version aus dem ersten Abschnitt, und die Stylesheet-Datei ändert sich überhaupt nicht.

    Kernpunkte

    • Betrachten Sie die Animationrichtung als Metadaten zu einem Update und nicht als Zustand einer Komponente; addTransitionType ist das dafür geeignete Werkzeug.
    • Nutzen Sie Typen über Objekt-Eigenschaften für grenzspezifische Zuordnungen, über :active-view-transition-type() für regelbasierte Abläufe auf der gesamten Seite oder über Ereignis-Callback-Funktionen für imperative Animationen.
    • Verwenden Sie unabhängige Typen, wie Richtung plus Geschwindigkeit, anstatt künstliche Klassennamen zu erfinden.
    • Vergessen Sie nicht die wichtigen Besonderheiten: Suspense entfernt die Typangaben, "none" überschreibt alles, und Typnamen teilen sich einen globalen Namensraum.
    • Zu den vollständigen API-Details siehe die Referenz zu addTransitionType sowie den Abschnitt zur typbasierten Anpassung in den Dokumentationen zu ViewTransition.