シングルページ アプリケーションでの同一ドキュメント ビュー遷移

公開日: 2021 年 8 月 17 日、最終更新日: 2024 年 9 月 25 日

単一のドキュメントでビュー遷移が実行される場合、それは同じドキュメントのビュー遷移と呼ばれます。これは通常、JavaScript を使用して DOM を更新するシングルページ アプリケーション(SPA)で発生します。Chrome 111 以降では、同一ドキュメントのビュー遷移がサポートされています。

同じドキュメントのビュー遷移をトリガーするには、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());
}

呼び出されると、ブラウザは view-transition-name CSS プロパティが宣言されているすべての要素のスナップショットを自動的にキャプチャします。

次に、渡されたコールバックを実行して DOM を更新し、新しい状態のスナップショットを取得します。

これらのスナップショットは、疑似要素のツリーに配置され、CSS アニメーションの機能を使用してアニメーション化されます。古い状態と新しい状態のスナップショットのペアが、古い位置とサイズから新しい位置にスムーズに移行し、コンテンツがクロスフェードします。必要に応じて、CSS を使用してアニメーションをカスタマイズできます。


デフォルトのトランジション: クロスフェード

デフォルトのビュー遷移はクロスフェードであるため、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 は DOM を新しい状態に変更します。これは任意の方法で行うことができます。たとえば、要素の追加や削除、クラス名の変更、スタイルの変更などを行うことができます。

ページがクロスフェードで切り替わります。

デフォルトのクロスフェード。最小限のデモ出典

クロスフェードはそれほど印象的ではありません。幸いなことに、トランジションはカスタマイズできますが、まず、この基本的なクロスフェードの仕組みを理解する必要があります。


これらのトランジションの仕組み

前のコードサンプルを更新しましょう。

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

.startViewTransition() が呼び出されると、API はページの現在の状態をキャプチャします。これには、スナップショットの作成も含まれます。

完了すると、.startViewTransition() に渡されたコールバックが呼び出されます。DOM が変更されるのはこの部分です。その後、API はページの新しい状態をキャプチャします。

新しい状態がキャプチャされると、API は次のような疑似要素ツリーを構築します。

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

::view-transition は、ページ上の他のすべての要素の上にオーバーレイで表示されます。これは、トランジションの背景色を設定する場合に便利です。

::view-transition-old(root) は古いビューのスクリーンショットで、::view-transition-new(root) は新しいビューのライブ表現です。どちらも CSS の「置換コンテンツ」(<img> など)としてレンダリングされます。

古いビューは opacity: 1 から opacity: 0 にアニメーションし、新しいビューは opacity: 0 から opacity: 1 にアニメーションして、クロスフェードを作成します。

アニメーションはすべて CSS アニメーションを使用して実行されるため、CSS でカスタマイズできます。

トランジションをカスタマイズする

ビュー遷移の擬似要素はすべて CSS でターゲットにできます。アニメーションは CSS を使用して定義されるため、既存の CSS アニメーション プロパティを使用して変更できます。次に例を示します。

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

この変更により、フェードが非常に遅くなります。

長いクロスフェード。最小限のデモ出典

まあ、それでも大したことではありません。代わりに、次のコードはマテリアル デザインの共有軸遷移を実装します。

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

結果は次のとおりです。

共有軸の移行。最小限のデモ出典

複数の要素を遷移する

前のデモでは、ページ全体が共有軸の移行に関与しています。これはページのほとんどの部分で機能しますが、見出しでは、スライドアウトしてすぐにスライドインするため、あまり適切ではありません。

これを回避するには、ヘッダーをページの残りの部分から抽出して、個別にアニメーション化できるようにします。これを行うには、要素に view-transition-name を割り当てます。

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

view-transition-name の値は任意の値にできます(none は遷移名がないことを意味するため、除きます)。遷移全体で要素を一意に識別するために使用されます。

その結果は次のとおりです。

ヘッダーが固定された共有軸の遷移。
最小限のデモ出典

ヘッダーが固定され、クロスフェードするようになりました。

この CSS 宣言により、疑似要素ツリーが変更されました。

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

これで、2 つのトランジション グループができました。1 つはヘッダー用、もう 1 つは残りの部分用です。これらは CSS で個別にターゲット設定し、異なるトランジションを適用できます。ただし、この場合、main-header はデフォルトのトランジション(クロスフェード)のままになっています。

デフォルトの遷移はクロスフェードだけではなく、::view-transition-group も遷移します。

  • 位置と変換(transform を使用)
  • 高さ

DOM の両側でヘッダーのサイズと位置が同じであるため、これまで問題はありませんでした。ヘッダー内のテキストを抽出することもできます。

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

fit-content を使用すると、要素は残りの幅まで拡大されず、テキストのサイズになります。これがないと、戻る矢印によって両方のページで同じサイズではなく、ヘッダー テキスト要素のサイズが縮小されます。

これで、3 つの部分を操作できるようになりました。

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

ただし、デフォルトを使用します。

スライドするヘッダー テキスト。最小限のデモ出典

見出しのテキストがスライドして、戻るボタンのスペースを確保します。


view-transition-class で複数の疑似要素を同じようにアニメーション化する

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.2.

Source

たとえば、多数のカードとページのタイトルを含むビュー遷移があるとします。タイトル以外のすべてのカードをアニメーション化するには、個々のカードをすべてターゲットにするセレクタを記述する必要があります。

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

20 個の要素がある場合20 個のセレクタを記述する必要があります。新しい要素を追加しますか?また、アニメーション スタイルを適用するセレクタも拡張する必要があります。スケーラビリティは高くありません。

view-transition-class は、ビュー遷移の疑似要素で使用して、同じスタイルルールを適用できます。

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

次のカードの例では、上記の CSS スニペットを使用しています。すべてのカード(新しく追加されたカードを含む)に、1 つのセレクタ html::view-transition-group(.card) で同じタイミングが適用されます。

カードのデモの録画。view-transition-class を使用すると、追加または削除されたカードを除くすべてのカードに同じ animation-timing-function が適用されます。

遷移をデバッグする

ビューの切り替えは CSS アニメーションに基づいて構築されているため、Chrome DevTools の [アニメーション] パネルは切り替えのデバッグに最適です。

[アニメーション] パネルを使用すると、次のアニメーションを一時停止してから、アニメーションを前後にスクラブできます。この間、遷移の疑似要素は [要素] パネルで確認できます。

Chrome DevTools でビューの切り替えをデバッグする。

移行する要素は同じ DOM 要素である必要はありません

これまでは、view-transition-name を使用して、ヘッダーとヘッダー内のテキストに個別のトランジション要素を作成してきました。これらは DOM の変更前と変更後で概念的には同じ要素ですが、そうでないトランジションを作成することもできます。

たとえば、メイン動画の埋め込みに view-transition-name を指定できます。

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

サムネイルがクリックされたら、遷移の間だけ同じ view-transition-name を付与できます。

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

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

結果は次のようになる

ある要素から別の要素への遷移。最小限のデモ出典

サムネイルがメイン画像に切り替わります。概念的(かつ文字通り)には異なる要素ですが、同じ view-transition-name を共有しているため、移行 API はこれらを同じものとして扱います。

このトランジションの実際のコードは、サムネイル ページに戻るトランジションも処理するため、上記の例よりも少し複雑になります。完全な実装については、ソースを参照してください。


カスタムの開始と終了のトランジション

次の例をご覧ください。

サイドバーの表示と非表示。最小限のデモ出典

サイドバーはトランジションの一部です。

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

ただし、前の例のヘッダーとは異なり、サイドバーはすべてのページに表示されません。両方の状態にサイドバーがある場合、遷移疑似要素は次のようになります。

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

ただし、サイドバーが新しいページにのみ存在する場合、::view-transition-old(sidebar) 疑似要素は存在しません。サイドバーの「古い」画像がないため、画像ペアには ::view-transition-new(sidebar) のみが含まれます。同様に、サイドバーが古いページにのみある場合、画像ペアには ::view-transition-old(sidebar) のみが含まれます。

前のデモでは、サイドバーが両方の状態に存在するかどうか、またはサイドバーが入力中か終了中かによって、サイドバーの切り替えが異なります。右からスライドしてフェードインし、右にスライドしてフェードアウトします。両方の状態に存在する場合は、その場に留まります。

特定のエントリ トランジションと終了トランジションを作成するには、:only-child 疑似クラスを使用して、画像ペアの唯一の子である古い疑似要素または新しい疑似要素をターゲットにします。

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

この場合、デフォルトが完璧であるため、サイドバーが両方の状態にある場合の特定のトランジションはありません。

非同期 DOM の更新とコンテンツの待機

.startViewTransition() に渡されたコールバックは Promise を返すことができます。これにより、非同期 DOM の更新や、重要なコンテンツの準備完了を待つことができます。

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

この移行は、Promise が完了するまで開始されません。この間、ページはフリーズしているため、遅延は最小限に抑える必要があります。具体的には、ネットワーク フェッチは .startViewTransition() コールバックの一部として行うのではなく、ページが完全にインタラクティブな状態のときに .startViewTransition() を呼び出す前に行う必要があります。

画像やフォントの準備が整うまで待つ場合は、アグレッシブなタイムアウトを使用してください。

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

ただし、遅延を完全に回避して、すでに持っているコンテンツを使用する方がよい場合もあります。


既存のコンテンツを最大限に活用する

サムネイルが拡大画像に切り替わる場合:

サムネイルが大きな画像に切り替わる。デモサイトをお試しください

デフォルトのトランジションはクロスフェードです。つまり、サムネイルはまだ読み込まれていないフル画像とクロスフェードする可能性があります。

この問題を解決する方法の 1 つは、完全な画像が読み込まれるまで待ってから、トランジションを開始することです。理想的には、.startViewTransition() を呼び出す前にこの処理を行うことで、ページがインタラクティブな状態を維持し、スピナーを表示して読み込み中であることをユーザーに知らせることができます。ただし、この場合はもっと良い方法があります。

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

これで、サムネイルはフェードアウトせず、フルサイズの画像の下に表示されるようになります。つまり、新しいビューが読み込まれていない場合、サムネイルはトランジション全体で表示されます。つまり、すぐに移行を開始でき、イメージ全体は独自のタイミングで読み込むことができます。

新しいビューに透明度がある場合はこの方法は機能しませんが、このケースでは透明度がないことがわかっているため、この最適化を行うことができます。

アスペクト比の変更を処理する

これまでの遷移はすべて同じアスペクト比の要素に対して行われてきましたが、必ずしもそうとは限りません。サムネイルが 1:1 で、メイン画像が 16:9 の場合はどうなりますか?

アスペクト比が変化しながら、ある要素が別の要素に遷移する。最小限のデモ出典

デフォルトのトランジションでは、グループは前のサイズから後のサイズにアニメーション化されます。古いビューと新しいビューは、グループの幅の 100% で、高さは自動です。つまり、グループのサイズに関係なくアスペクト比が維持されます。

これは適切なデフォルトですが、このケースでは望ましくありません。それによって次のようになります。

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

つまり、幅が拡大してもサムネイルは要素の中央に留まりますが、1:1 から 16:9 に移行するにつれて、画像全体が切り抜きから外れます。

詳しくは、ビューの切り替え: アスペクト比の変更の処理をご覧ください。


メディアクエリを使用して、さまざまなデバイスの状態に合わせてトランジションを変更する

モバイルとパソコンで異なるトランジションを使用することもできます。たとえば、モバイルでは横からスライドインするトランジションを、パソコンではより控えめなスライドインを使用する例を次に示します。

ある要素から別の要素への遷移。最小限のデモ出典

これは、通常のメディアクエリを使用して実現できます。

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

一致するメディアクエリに応じて、view-transition-name を割り当てる要素を変更することもできます。


「モーションを抑制」の設定に対応する

ユーザーはオペレーティング システムでモーションの低減を希望することを指定でき、その設定は CSS で公開されます。

これらのユーザーの移行をすべて禁止することもできます。

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

ただし、「モーションを減らす」設定は、モーションを完全にオフにすることを意味するわけではありません。上記のスニペットの代わりに、より控えめなアニメーションを選択することもできますが、要素間の関係とデータの流れを表現するアニメーションを選択する必要があります。


ビューの切り替えタイプで複数のビューの切り替えスタイルを処理する

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.

Source

特定のビューから別のビューへの遷移に、特別に調整された遷移が必要になることがあります。たとえば、ページネーション シーケンスで次のページまたは前のページに移動するときに、シーケンス内の上位のページに移動するか下位のページに移動するかに応じて、コンテンツを別の方向にスライドさせることができます。

ページネーション デモの録画。移動先のページに応じて異なるトランジションを使用します。

これにはビュー遷移タイプを使用できます。ビュー遷移タイプを使用すると、1 つ以上のアクティブなビュー遷移にタイプを割り当てることができます。たとえば、ページネーション シーケンスで上位のページに移動する場合は forwards タイプを使用し、下位のページに移動する場合は backwards タイプを使用します。これらのタイプは、キャプチャまたはトランジションの実行時にのみ有効になり、CSS を通じてカスタマイズして、それぞれ異なるアニメーションを使用できます。

同じドキュメントのビュー遷移で型を使用するには、typesstartViewTransition メソッドに渡します。これを可能にするため、document.startViewTransition はオブジェクトも受け入れます。update は DOM を更新するコールバック関数、types は型を含む配列です。

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});
を使用します。

これらのタイプに対応するには、:active-view-transition-type() セレクタを使用します。ターゲットにする type をセレクタに渡します。これにより、複数のビュー遷移のスタイルを互いに分離した状態に保つことができ、一方の宣言が他方の宣言を妨げることはありません。

型はキャプチャまたは遷移の実行時にのみ適用されるため、セレクタを使用して、その型のビュー遷移でのみ要素の view-transition-name を設定または設定解除できます。

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

次のページネーションのデモでは、移動先のページ番号に基づいてページ コンテンツが前後にスライドします。タイプはクリック時に決定され、document.startViewTransition に渡されます。

タイプに関係なく、アクティブなビュー遷移をターゲットにするには、代わりに :active-view-transition 疑似クラス セレクタを使用します。

html:active-view-transition {
    
}

ビュー遷移ルートのクラス名を使用して複数のビュー遷移スタイルを処理する

特定の種類のビューから別の種類のビューへの切り替えには、特別に調整された切り替えが必要になることがあります。また、「戻る」ナビゲーションは「進む」ナビゲーションと異なる必要があります。

「戻る」際の異なるトランジション。最小限のデモ出典

遷移タイプが導入される前は、このようなケースに対処するために、遷移ルートに一時的にクラス名を設定していました。document.startViewTransition を呼び出す場合、このトランジション ルートは <html> 要素です。JavaScript で document.documentElement を使用してアクセスできます。

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

遷移の完了後にクラスを削除するために、この例では transition.finished を使用しています。これは、遷移が最終状態に達すると解決される Promise です。このオブジェクトのその他のプロパティについては、API リファレンスをご覧ください。

これで、CSS でそのクラス名を使用してトランジションを変更できるようになりました。

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

メディアクエリと同様に、これらのクラスの存在を使用して、view-transition-name を取得する要素を変更することもできます。


他のアニメーションをフリーズせずにトランジションを実行する

動画の位置が切り替わるデモをご覧ください。

動画の切り替え。最小限のデモ出典

何か問題がありましたか?ご安心ください。以下は、速度を落としたものです。

動画のトランジション、遅く。最小限のデモ出典

切り替え中は動画が一時停止したように見えますが、その後、再生中の動画がフェードインします。これは、::view-transition-old(video) が古いビューのスクリーンショットであるのに対し、::view-transition-new(video) は新しいビューのライブ画像であるためです。

この問題は修正できますが、まず修正する価値があるかどうかを検討してください。トランジションが通常の速度で再生されているときに「問題」が見られなかった場合は、変更する必要はありません。

本当に修正したい場合は、::view-transition-old(video) を表示せず、::view-transition-new(video) に直接切り替えます。これを行うには、デフォルトのスタイルとアニメーションをオーバーライドします。

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

手順は以上です。

動画のトランジション、遅く。最小限のデモ出典

動画がトランジション全体で再生されるようになりました。


Navigation API(およびその他のフレームワーク)との統合

ビューの切り替えは、他のフレームワークやライブラリと統合できるように指定されています。たとえば、シングルページ アプリケーション(SPA)でルーターを使用している場合は、ルーターの更新メカニズムを調整して、ビューの切り替えを使用してコンテンツを更新できます。

このページネーションのデモから抜粋した次のコード スニペットでは、ビューの切り替えがサポートされている場合に document.startViewTransition を呼び出すように Navigation API のインターセプト ハンドラが調整されています。

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

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

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

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

一部のブラウザでは、ユーザーがスワイプ ジェスチャーでナビゲーションを行うときに独自のトランジションが提供されます。その場合は、ユーザー エクスペリエンスの低下や混乱を招くため、独自のビュー遷移をトリガーしないでください。ユーザーには、ブラウザが提供するトランジションと、あなたが提供するトランジションの 2 つが連続して実行されるように見えます。

そのため、ブラウザが独自の視覚的トランジションを提供している場合は、ビュー トランジションが開始されないようにすることをおすすめします。これを行うには、NavigateEvent インスタンスの hasUAVisualTransition プロパティの値を確認します。ブラウザが視覚的なトランジションを提供している場合、プロパティは true に設定されます。この hasUIVisualTransition プロパティは PopStateEvent インスタンスにも存在します。

前のスニペットでは、ビューの切り替えを実行するかどうかを判断するチェックでこのプロパティが考慮されています。同一ドキュメントのビュー遷移がサポートされていない場合や、ブラウザが独自の遷移をすでに提供している場合、ビュー遷移はスキップされます。

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

次の録画では、ユーザーがスワイプして前のページに戻っています。左側のキャプチャには、hasUAVisualTransition フラグのチェックが含まれていません。右側の録音にはチェックが含まれているため、ブラウザが視覚的な切り替えを提供したため、手動のビュー切り替えはスキップされます。

hasUAVisualTransition
のチェックがない場合(左)とある場合(右)の同じサイトの比較

JavaScript を使用したアニメーション

これまでのところ、すべてのトランジションは CSS を使用して定義されていますが、CSS だけでは不十分な場合があります。

円形のトランジション。最小限のデモ出典

このトランジションの一部は、CSS だけでは実現できません。

  • アニメーションはクリックした場所から始まります。
  • アニメーションは、円の半径が最も遠い角まで伸びたところで終了します。ただし、将来的には CSS で可能になることが期待されます。

幸いなことに、Web Animation API を使用してトランジションを作成できます。

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

この例では、遷移疑似要素が正常に作成されると解決されるプロミスである transition.ready を使用します。このオブジェクトのその他のプロパティについては、API リファレンスをご覧ください。


拡張機能としてのトランジション

View Transition API は、DOM の変更をラップして、その変更のトランジションを作成するように設計されています。ただし、このトランジションは拡張機能として扱う必要があります。つまり、DOM の変更が成功してもトランジションが失敗した場合、アプリは「エラー」状態にならないようにする必要があります。理想的には移行が失敗しないようにすべきですが、失敗した場合でも、ユーザー エクスペリエンスの残りの部分が損なわれないようにする必要があります。

トランジションを拡張機能として扱うには、トランジションが失敗した場合にアプリが例外をスローするような方法でトランジション Promise を使用しないように注意してください。

すべきでないこと
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)',
    }
  );
}

この例の問題は、遷移が ready 状態に到達できない場合、switchView() が拒否されることです。ただし、これはビューの切り替えが失敗したことを意味するわけではありません。DOM は正常に更新されましたが、view-transition-name が重複していたため、トランジションはスキップされました。

この場合は次のように対応してください。

すべきこと
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
  }
}

この例では、transition.updateCallbackDone を使用して DOM の更新を待機し、失敗した場合は拒否します。遷移が失敗した場合に switchView が拒否されなくなり、DOM の更新が完了すると解決され、失敗すると拒否されるようになりました。

新しいビューが「落ち着いた」とき(アニメーション遷移が完了したか、最後までスキップされたとき)に switchView を解決する場合は、transition.updateCallbackDonetransition.finished に置き換えます。


ポリフィルではありませんが…

これは簡単にポリフィルできる機能ではありません。ただし、このヘルパー関数を使用すると、ビューの切り替えをサポートしていないブラウザでも簡単に処理できます。

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

次のように使用できます。

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

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

  // …
}

ビューの切り替えに対応していないブラウザでは、updateDOM は呼び出されますが、アニメーションによる切り替えは行われません。

また、移行中に <html> に追加する classNames を指定することもできます。これにより、ナビゲーションのタイプに応じて移行を変更するのが容易になります。

ビューの切り替えをサポートするブラウザでも、アニメーションを不要にする場合は、trueskipTransition に渡すこともできます。これは、サイトにトランジションを無効にするユーザー設定がある場合に役立ちます。


フレームワークの操作

DOM の変更を抽象化するライブラリやフレームワークを使用している場合、DOM の変更が完了したタイミングを把握するのが難しいことがあります。さまざまなフレームワークで 上記のヘルパーを使用した例を次に示します。

  • React - ここでの鍵は flushSync です。これは、一連の状態変更を同期的に適用します。はい。その API の使用に関する大きな警告が表示されますが、Dan Abramov 氏が、このケースでは適切だと保証してくれました。React と非同期コードの場合と同様に、startViewTransition から返されるさまざまな Promise を使用する場合は、コードが正しい状態で実行されていることを確認してください。
  • Vue.js - ここでのキーは nextTick です。これは、DOM が更新されると一度だけ実行されます。
  • Svelte - Vue とよく似ていますが、次の変更を待つメソッドは tick です。
  • Lit - ここでのポイントは、コンポーネント内の this.updateComplete プロミスです。これは、DOM が更新されると完了します。
  • Angular - ここでのキーは applicationRef.tick で、保留中の DOM 変更をフラッシュします。Angular バージョン 17 以降では、@angular/router に付属する withViewTransitions を使用できます

API リファレンス

const viewTransition = document.startViewTransition(update)

新しい ViewTransition を開始します。

update は、ドキュメントの現在の状態がキャプチャされた後に呼び出される関数です。

次に、updateCallback から返された Promise が解決されると、次のフレームでトランジションが開始されます。updateCallback によって返された Promise が拒否されると、トランジションは破棄されます。

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

指定された型で新しい ViewTransition を開始する

ドキュメントの現在の状態がキャプチャされると、update が呼び出されます。

types は、遷移をキャプチャまたは実行するときに、遷移のアクティブなタイプを設定します。最初は空です。詳しくは、下記の viewTransition.types をご覧ください。

ViewTransition のインスタンス メンバー:

viewTransition.updateCallbackDone

updateCallback によって返された Promise が fulfilled になると fulfilled になり、rejected になると rejected になる Promise。

View Transition API は DOM の変更をラップし、トランジションを作成します。ただし、場合によっては、トランジション アニメーションの成功や失敗は気にせず、DOM の変更がいつ発生したかを知りたいだけの場合があります。updateCallbackDone は、そのユースケース用です。

viewTransition.ready

遷移の疑似要素が作成され、アニメーションが開始されようとしているときに解決される Promise。

遷移を開始できない場合は拒否します。これは、重複する view-transition-name などの構成ミスが原因であるか、updateCallback が拒否された Promise を返したことが原因である可能性があります。

これは、JavaScript で遷移擬似要素をアニメーション化する場合に便利です。

viewTransition.finished

最終状態がユーザーに完全に表示され、操作可能になったときに解決される Promise。

updateCallback が拒否された Promise を返した場合にのみ拒否されます。これは、終了状態が作成されなかったことを示します。

それ以外の場合、遷移が開始されなかったり、遷移中にスキップされたりしても、終了状態には到達するため、finished は満たされます。

viewTransition.types

アクティブなビュー遷移のタイプを保持する Set のようなオブジェクト。エントリを操作するには、インスタンス メソッド clear()add()delete() を使用します。

CSS で特定の型に応答するには、トランジション ルートで :active-view-transition-type(type) 疑似クラス セレクタを使用します。

ビューの切り替えが完了すると、型は自動的にクリーンアップされます。

viewTransition.skipTransition()

トランジションのアニメーション部分をスキップします。

DOM の変更はトランジションとは別であるため、この場合 updateCallback の呼び出しはスキップされません。


デフォルトのスタイルとトランジションのリファレンス

::view-transition
ビューポート全体に表示され、各 ::view-transition-group を含むルート疑似要素。
::view-transition-group

絶対位置指定。

「前」の状態と「後」の状態の間で widthheight を切り替えます。

ビューポート空間の「前」と「後」の四角形の間を transform で遷移します。

::view-transition-image-pair

グループを埋めるように絶対配置されます。

isolation: isolate を使用して、古いビューと新しいビューに対する mix-blend-mode の影響を制限します。

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

ラッパーの左上に絶対配置されます。

グループの幅を 100% 埋めますが、高さは自動に設定されているため、グループを埋めるのではなく、アスペクト比が維持されます。

mix-blend-mode: plus-lighter を使用して、真のクロスフェードを可能にします。

古いビューが opacity: 1 から opacity: 0 に移行します。新しいビューが opacity: 0 から opacity: 1 に切り替わります。


フィードバック

デベロッパーの皆様からのフィードバックをお待ちしております。その場合は、GitHub で CSS Working Group に問題を報告し、提案や質問をお送りください。事象の先頭に [css-view-transitions] を付けます。

バグが発生した場合は、代わりに Chromium バグを報告してください。