View Transitions API によるスムーズでシンプルな遷移

対応ブラウザ

  • 111
  • 111
  • x
  • x

ソース

View Transition API を使用すると、1 つのステップで DOM を簡単に変更でき、2 つの状態間の遷移をアニメーションで作成できます。この機能は Chrome 111 以降で利用できます。

View Transition API で作成された遷移。デモサイトを試す - Chrome 111 以降が必要です。

この機能が必要な理由

ページ遷移は見栄えが良いだけでなく、フローの方向を伝え、ページ間の関連性が高い要素を明確にします。データの取得中にも発生する可能性があり、パフォーマンスの認知が速くなります。

しかし、CSS トランジションCSS アニメーションWeb Animation API などのアニメーション ツールはすでにウェブ上で公開されています。では、何かを移動するために新しいツールが必要なのはなぜですか?

実際、状態遷移は、既存のツールであっても難しいものです。

単純なクロスフェードのようにも、両方の状態が同時に存在する必要があります。送信要素で追加のインタラクションを処理するなど、ユーザビリティの面で課題が生じます。また、支援デバイスのユーザーの場合、変更前と変更後の両方の状態が DOM に同時に存在し、ツリーの周りをきれいに動き回る場合がありますが、読み取りの位置やフォーカスが失われやすくなる可能性があります。

スクロール位置の 2 つの状態が異なる場合、状態変化の処理は特に困難になります。また、あるコンテナから別のコンテナに要素を移動する場合、overflow: hidden などのクリッピングで問題が発生する可能性があります。つまり、目的の効果を得るには CSS を再構築する必要があります。

不可能ではありません。ただとても困難です。

ビュー遷移を使用すると、状態間で重複することなく DOM の変更を行うことができ、スナップショットされたビューを使用して状態間の遷移アニメーションを作成できるので、より簡単な方法です。

さらに、現在の実装はシングルページ アプリ(SPA)を対象としていますが、この機能が拡張され、ページ全体の読み込み間の移行も可能になりますが、これは現時点では不可能です。

標準化のステータス

この機能は、W3C CSS ワーキング グループドラフト仕様として開発中です。

API の設計に満足したら、この機能を安定版に移行するために必要なプロセスとチェックを開始します。

デベロッパーからのフィードバックは非常に重要です。提案や質問を GitHub で問題を報告してください。

最もシンプルなトランジション: クロスフェード

デフォルトのビュー遷移はクロスフェードなので、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;
}

この 1 つの変更により、フェードはかなり遅くなりました。

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

すばらしいですね。代わりに、マテリアル デザインの共有軸の移行を実装しましょう。

@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)
   └─ …

繰り返しになりますが、デフォルトのままにしておきます。

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

これで、見出しテキストが少し満足のいくスライドになり、[戻る] ボタンのスペースを確保できるようになりました。

遷移のデバッグ

ビュー遷移は CSS アニメーションの上に作成されるため、遷移のデバッグには Chrome DevTools の [Animations] パネルが適しています。

[Animations] パネルで、次のアニメーションを一時停止してから、アニメーション内を前後にスクラブできます。この間、遷移の疑似要素は [要素] パネルに表示されます。

Chrome デベロッパー ツールを使用してビュー遷移をデバッグする

遷移する要素は同じ 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();
  });
};

結果は次のようになります。

ある要素が別の要素に遷移しています。最小限のデモソース

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

実際のコードは、上記の単純な例よりも少し複雑です。サムネイル ページへの遷移も処理するためです。完全な実装については、ソースをご覧ください

カスタムの開始と終了の遷移

次の例をご覧ください。

サイドバーの開始と終了。最小限のデモソース

サイドバーは移行の一部です。

.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) 疑似要素はありません。サイドバーに「古い」画像がないため、image-pair には ::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;
  }
}

ただし、「動きの低減」が望ましいからといって、ユーザーが動きをしないわけではありません。上の画像の代わりに、より繊細なアニメーションを選択することもできますが、それでも要素間の関係とデータの流れを表現するものを選びます。

ナビゲーションの種類に応じて遷移を変更する

ある特定の種類のページから別のページへのナビゲーションには、特別にカスタマイズされた遷移が必要です。「戻る」ナビゲーションと「進む」ナビゲーションは別のものにする必要があります。

「戻る」場合のさまざまな遷移。最小限のデモソース

このようなケースに対処する最善の方法は、<html> にクラス名を設定することです。これはドキュメント要素とも呼ばれます。

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

これで完了です。

動画トランジション、低速。最小限のデモソース

これで、移行中に動画が再生されます。

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

この例では、遷移擬似要素が正常に作成されると解決される Promise である 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,
  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;
}

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

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

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

  // …
}

ビュー遷移をサポートしていないブラウザでも、updateDOM は呼び出されますが、アニメーション遷移は表示されません。

また、遷移中に <html> に追加する classNames を指定して、簡単にナビゲーションの種類に応じて遷移を変更することもできます。

ビュー遷移をサポートしているブラウザでも、アニメーションを望まない場合は、trueskipTransition に渡します。これは、サイトで移行を無効にするユーザー設定がある場合に便利です。

フレームワークの操作

DOM の変更を抽象化するライブラリやフレームワークを使用している場合、厄介なのは DOM の変更が完了したかどうかを知ることです。さまざまなフレームワークで上記のヘルパーを使用した例を次に示します。

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

API リファレンス

const viewTransition = document.startViewTransition(updateCallback)

新しい ViewTransition を開始します。

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

その後、updateCallback から返された Promise が解決されると、次のフレームで遷移が開始されます。updateCallback から返された Promise が拒否された場合、遷移は破棄されます。

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

viewTransition.updateCallbackDone

updateCallback から返された Promise が履行された場合に履行され、拒否された Promise が拒否される Promise。

View Transition API は、DOM の変更をラップして遷移を作成します。しかし、トランジション アニメーションの成功と失敗は気にせず、DOM の変化がいつ起きるかを知りたいという場合もあります。updateCallbackDone はそのユースケース向けです。

viewTransition.ready

遷移の擬似要素が作成されてアニメーションが開始されるときに実行される Promise 。

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

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

viewTransition.finished

最終状態が完全に表示され、ユーザーに操作が行われた時点で履行される Promise。

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

それ以外の場合、遷移の開始に失敗した場合や、遷移中にスキップされた場合でも、終了状態に達するため、finished が処理されます。

viewTransition.skipTransition()

遷移のアニメーション部分をスキップします。

DOM の変更は遷移とは別であるため、これによって updateCallback の呼び出しがスキップされることはありません。

デフォルトのスタイルと遷移のリファレンス

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

絶対的な位置付け。

「前」と「後」の状態間の widthheight の遷移。

ビューポート空間の「前」と「後」のクワッド間の遷移 transform

::view-transition-image-pair

グループ全体に配置される。

元のビューと新しいビューに対する plus-lighter ブレンドモードの効果を制限する isolation: isolate があります。

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

ラッパーの左上に配置される。

グループの幅の 100% になりますが、高さは自動的に設定されるため、グループ全体に表示されるのではなく、アスペクト比が維持されます。

真のクロスフェードを可能にする mix-blend-mode: plus-lighter があります。

古いビューは opacity: 1 から opacity: 0 に移行します。新しいビューは opacity: 0 から opacity: 1 に移行します。

フィードバック

この段階では、デベロッパーからのフィードバックは非常に重要であるため、提案や質問を GitHub で問題を報告してください。