マルチページ アプリケーションのドキュメント間のビュー遷移

2 つの異なるドキュメント間でビューの遷移が発生することを、ドキュメント間のビュー遷移と呼びます。これは通常、マルチページ アプリケーション(MPA)に当てはまります。Chrome 126 以降の Chrome では、ドキュメント間のビューの移行がサポートされます。

対応ブラウザ

  • Chrome: 126。 <ph type="x-smartling-placeholder">
  • Edge: 126。 <ph type="x-smartling-placeholder">
  • Firefox: サポートされていません。 <ph type="x-smartling-placeholder">
  • Safari: サポートされていません。 <ph type="x-smartling-placeholder">

ドキュメント間のビュー遷移では、同一ドキュメントでのビュー遷移とまったく同じ構成要素と原則が使用されますが、これは非常に意図的なものです。

  1. ブラウザは、元のページと新しいページの両方で、view-transition-name が一意である要素のスナップショットを作成します。
  2. レンダリングが抑制されている間に DOM が更新されます。
  3. 最後に、遷移は CSS アニメーションを利用しています。
で確認できます。

同一ドキュメント ビュー遷移と違うのは、ドキュメント間でビュー遷移を行う場合、ビュー遷移を開始するために document.startViewTransition を呼び出す必要がない点です。代わりに、ドキュメント間ビュー遷移のトリガーは、あるページから別のページへの同一オリジンのナビゲーションです。これは通常、ウェブサイトのユーザーがリンクをクリックすることで行われるアクションです。

つまり、2 つのドキュメント間のビュー移行を開始するために呼び出す API はありません。ただし、次の 2 つの条件を満たす必要があります。

  • 両方のドキュメントが同じオリジンに存在する必要があります。
  • どちらのページでも、ビューの移行を許可するにはオプトインする必要があります。

これらの条件については、このドキュメントの後半で説明します。


ドキュメント間のビュー遷移は同一オリジンのナビゲーションに限定される

ドキュメント間のビュー遷移は、同一オリジン ナビゲーションのみに制限されます。登録されている両方のページのオリジンが同じ場合、ナビゲーションは同じオリジンとみなされます。

ページの送信元は、使用されているスキーム、ホスト名、ポートの組み合わせです(web.dev の詳細を参照)。

<ph type="x-smartling-placeholder">
</ph> スキーム、ホスト名、ポートがハイライト表示された URL の例。これらが組み合わさって原点を形成します。
スキーム、ホスト名、ポートがハイライト表示された URL の例。これらが組み合わさって起点を形成します。

たとえば、developer.chrome.com から developer.chrome.com/blog に移動する場合、これらは同一生成元であるため、ドキュメント間ビューをまたいで遷移できます。 developer.chrome.com から www.chrome.com に移動する場合は、クロスオリジンかつ同一サイトであるため、そのような移行はできません。


ドキュメント間のビュー移行のオプトイン

2 つのドキュメント間でドキュメントをまたいでビューを移行するには、両方の登録ページでこの移行を許可する必要があります。これは、CSS の @view-transition アットルールで行われます。

@view-transition アットルールで、navigation 記述子を auto に設定して、ドキュメント間、同一オリジン ナビゲーションでビュー遷移を有効にします。

@view-transition {
  navigation: auto;
}

navigation 記述子を auto に設定すると、次の NavigationType に対してビュー遷移を発生させることを許可することになります。

  • traverse
  • push または replace: ユーザーがブラウザの UI メカニズムを使用して有効化を開始した場合。

auto から除外されるナビゲーションには、URL アドレスバーを使用した移動やブックマークのクリックのほか、ユーザーまたはスクリプトによって開始される再読み込みなどが該当します。

ナビゲーションに時間がかかりすぎる(Chrome の場合は 4 秒を超える)場合、ビュー遷移は TimeoutError DOMException でスキップされます。

ドキュメント間のビュー遷移のデモ

ビュー遷移を使用して Stack Navigator のデモを作成するデモをご覧ください。ここでは document.startViewTransition() の呼び出しはなく、あるページから別のページに移動することでビュー遷移がトリガーされます。

<ph type="x-smartling-placeholder">
</ph>
Stack Navigator のデモの録画Chrome 126 以降が必要です。

ドキュメント間のビュー遷移をカスタマイズする

ドキュメント間のビュー遷移をカスタマイズする場合、使用できるウェブ プラットフォームの機能があります。

これらの機能は View Transition API の仕様そのものではありませんが、一緒に使用するように設計されています。

pageswap イベントと pagereveal イベント

対応ブラウザ

  • Chrome: 124。 <ph type="x-smartling-placeholder">
  • Edge: 124。 <ph type="x-smartling-placeholder">
  • Firefox: サポートされていません。 <ph type="x-smartling-placeholder">
  • Safari: サポートされていません。 <ph type="x-smartling-placeholder">

ソース

ドキュメント間のビュー遷移をカスタマイズできるように、HTML 仕様には pageswappagereveal という 2 つの新しいイベントが含まれています。

この 2 つのイベントは、ビュー遷移が発生するかどうかにかかわらず、同一オリジンのクロスドキュメント ナビゲーションのたびに発生します。2 つのページ間でビューが遷移しようとしている場合は、これらのイベントの viewTransition プロパティを使用して、ViewTransition オブジェクトにアクセスできます。

  • pageswap イベントは、ページの最後のフレームがレンダリングされる前に呼び出されます。これを使用すると、古いスナップショットが取得される直前に、送信ページの土壇場での変更を行うことができます。
  • pagereveal イベントは、ページが初期化または再度有効化された後、最初のレンダリングが発生する前に呼び出されます。これにより、新しいスナップショットが取得される前に新しいページをカスタマイズできます。

たとえば、これらのイベントを使用して、一部の view-transition-name 値をすばやく設定または変更したり、sessionStorage に対してデータを読み書きすることでドキュメント間でデータを渡し、実際に実行する前にビュー遷移をカスタマイズしたりできます。

let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
  if (event.target.tagName.toLowerCase() === 'a') return;
  lastClickX = event.clientX;
  lastClickY = event.clientY;
});

// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
  if (event.viewTransition && lastClick) {
    sessionStorage.setItem('lastClickX', lastClickX);
    sessionStorage.setItem('lastClickY', lastClickY);
  }
});

// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
  if (event.viewTransition) {
    lastClickX = sessionStorage.getItem('lastClickX');
    lastClickY = sessionStorage.getItem('lastClickY');
  }
});

必要に応じて、両方のイベントで遷移をスキップすることもできます。

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    if (goodReasonToSkipTheViewTransition()) {
      e.viewTransition.skipTransition();
    }
  }
}

pageswappagerevealViewTransition オブジェクトは、それぞれ異なるオブジェクトです。また、さまざまな Promise の扱い方も異なります。

  • pageswap: ドキュメントが非表示になると、古い ViewTransition オブジェクトはスキップされます。その場合、viewTransition.ready は拒否され、viewTransition.finished は解決されます。
  • pagereveal: この時点で updateCallBack Promise はすでに解決されています。viewTransition.ready Promise と viewTransition.finished Promise を使用できます。

対応ブラウザ

  • Chrome: 123。 <ph type="x-smartling-placeholder">
  • Edge: 123。 <ph type="x-smartling-placeholder">
  • Firefox: サポートされていません。 <ph type="x-smartling-placeholder">
  • Safari: サポートされていません。 <ph type="x-smartling-placeholder">

ソース

pageswap イベントと pagereveal イベントのどちらでも、元のページと新しいページの URL に基づいてアクションを実行できます。

たとえば、MPA スタック ナビゲーターの場合、使用するアニメーションのタイプはナビゲーション パスによって異なります。

  • 概要ページから詳細ページに移動するときは、新しいコンテンツを右から左にスライドインする必要があります。
  • 詳細ページから概要ページに移動する際、古いコンテンツを左から右にスライドアウトする必要があります。

そのためには、pageswap の場合はこれから、または pagereveal の場合は、ちょうど起きたばかりのナビゲーションに関する情報が必要です。

そのため、ブラウザで同一オリジン ナビゲーションに関する情報を保持する NavigationActivation オブジェクトを公開できるようになりました。このオブジェクトは、Navigation API の navigation.entries() にある、使用されたナビゲーション タイプ、現在のデスティネーション履歴エントリ、最終デスティネーション履歴エントリを公開します。

有効なページでは、navigation.activation を介してこのオブジェクトにアクセスできます。pageswap イベントでは、e.activation を介してアクセスできます。

こちらのプロフィールのデモでは、pageswap イベントと pagereveal イベントの NavigationActivation 情報を使用して、ビュー遷移に参加する必要がある要素に view-transition-name 値を設定できます。

そうすれば、リスト内のすべてのアイテムを最初に view-transition-name で装飾する必要がなくなります。この処理は JavaScript を使用し、それを必要とする要素に対してのみ行われます。

<ph type="x-smartling-placeholder">
</ph>
プロフィールのデモの録画Chrome 126 以降が必要です。

コードは次のとおりです。

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove view-transition-names after snapshots have been taken
      // (this to deal with BFCache)
      await e.viewTransition.finished;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove names after snapshots have been taken
      // so that we're ready for the next navigation
      await e.viewTransition.ready;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

また、コードはビュー遷移の実行後に view-transition-name 値を削除することで、自身のクリーンアップも行います。このようにして、ページを連続して移動する準備が整い、履歴のトラバーサルを処理できるようになります。

その場合は、view-transition-name を一時的に設定するこのユーティリティ関数を使用します。

const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = name;
  }

  await vtPromise;

  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = '';
  }
}

上記のコードは、次のように単純化できます。

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      // Clean up after the page got replaced
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.finished);
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      // Clean up after the snapshots have been taken
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.ready);
    }
  }
});

レンダリング ブロックを使用してコンテンツが読み込まれるのを待つ

対応ブラウザ

  • Chrome: 124。 <ph type="x-smartling-placeholder">
  • Edge: 124。 <ph type="x-smartling-placeholder">
  • Firefox: サポートされていません。 <ph type="x-smartling-placeholder">
  • Safari: サポートされていません。 <ph type="x-smartling-placeholder">

場合によっては、新しい DOM に特定の要素が表示されるまで、ページの最初のレンダリングを保留したいことがあります。これにより、フラッシュを回避し、アニメーション化する状態が安定するようにします。

<head> で次のメタタグを使用して、ページが最初にレンダリングされる前に存在する必要がある 1 つ以上の要素 ID を定義します。

<link rel="expect" blocking="render" href="#section1">

このメタタグは、コンテンツを読み込むのではなく、要素が DOM に存在する必要があることを意味します。たとえば画像の場合、id が指定された <img> タグが DOM ツリーに存在するだけで、条件が true と評価されます。画像自体が読み込み中の可能性があります。

レンダリング ブロックを全面的に適用する前に、増分レンダリングはウェブの基本要素であることに留意してください。そのため、レンダリングのブロックを設定する際は十分に注意してください。レンダリングのブロックによる影響は状況に応じて評価する必要があります。デフォルトでは、Core Web Vitals への影響を測定することでユーザーへの影響を積極的に測定して測定できる場合を除き、blocking=render は使用しません。


ドキュメント間のビュー遷移で遷移タイプを表示する

ドキュメント間のビュー遷移では、アニメーションやキャプチャする要素をカスタマイズするためのビュー遷移タイプもサポートされています。

たとえば、ページネーションで次のページまたは前のページに移動する場合、移動順序の上位ページと下位ページのどちらに移動するかに応じて、異なるアニメーションを使用できます。

これらのタイプを事前に設定するには、@view-transition at-rule にタイプを追加します。

@view-transition {
  navigation: auto;
  types: slide, forwards;
}

その場で型を設定するには、pageswap イベントと pagereveal イベントを使用して e.viewTransition.types の値を操作します。

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
    e.viewTransition.types.add(transitionType);
  }
});

型は、古いページの ViewTransition オブジェクトから新しいページの ViewTransition オブジェクトに自動的には引き継がれません。アニメーションを期待どおりに実行するには、少なくとも新しいページで使用するタイプを決定する必要があります。

これらの型に対応するには、同一ドキュメント ビュー遷移の場合と同じように :active-view-transition-type() 疑似クラス セレクタを使用します。

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only */
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;
  }
}

型はアクティブ ビュー遷移にのみ適用されるため、ビュー遷移が終了すると、タイプは自動的にクリーンアップされます。そのため、型は BFCache などの機能とうまく連携します。

デモ

次のページ分けのデモでは、移動先のページ番号に基づいてページ コンテンツが前後にスライドします。

<ph type="x-smartling-placeholder">
</ph>
ページ分けデモ(MPA)の録画表示するページに応じて、切り替え方法が異なります。

使用する切り替えの種類は、pagereveal イベントと pageswap イベントでリンク先 URL から判断されます。

const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
  const currentURL = new URL(fromNavigationEntry.url);
  const destinationURL = new URL(toNavigationEntry.url);

  const currentPathname = currentURL.pathname;
  const destinationPathname = destinationURL.pathname;

  if (currentPathname === destinationPathname) {
    return "reload";
  } else {
    const currentPageIndex = extractPageIndexFromPath(currentPathname);
    const destinationPageIndex = extractPageIndexFromPath(destinationPathname);

    if (currentPageIndex > destinationPageIndex) {
      return 'backwards';
    }
    if (currentPageIndex < destinationPageIndex) {
      return 'forwards';
    }

    return 'unknown';
  }
};

フィードバック

デベロッパーからのフィードバックを常に歓迎いたします。共有するには、GitHub の CSS ワーキング グループに問題を提出し、提案や質問を送信してください。問題の接頭辞に [css-view-transitions] を付けます。 バグが発生した場合は、代わりに Chromium のバグを報告してください。