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

2 つの異なるドキュメント間でビューの切り替えが行われる場合、それはドキュメント間のビューの切り替えと呼ばれます。これは通常、マルチページ アプリケーション(MPA)の場合です。クロスドキュメント ビューの切り替えは、Chrome 126 以降の Chrome でサポートされています。

Browser Support

  • Chrome: 126.
  • Edge: 126.
  • Firefox: not supported.
  • Safari: 18.2.

Source

ドキュメント間のビュー遷移は、ドキュメント内のビュー遷移と同じビルディング ブロックと原則に依存しています。これは意図的なものです。

  1. ブラウザは、古いページと新しいページの両方で一意の view-transition-name を持つ要素のスナップショットを取得します。
  2. レンダリングが抑制されている間に DOM が更新されます。
  3. 最後に、トランジションは CSS アニメーションによって実現されています。

同じドキュメント内のビュー遷移との違いは、ドキュメント間のビュー遷移では document.startViewTransition を呼び出してビュー遷移を開始する必要がないことです。クロスドキュメント ビュー遷移のトリガーは、あるページから別のページへの同一オリジン ナビゲーションです。これは通常、ウェブサイトのユーザーがリンクをクリックすることで実行されるアクションです。

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

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

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


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

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

ページのオリジンは、使用されるスキーム、ホスト名、ポートの組み合わせです(web.dev で詳しく説明されています)。

スキーム、ホスト名、ポートがハイライト表示された URL の例。これらを組み合わせたものがオリジンになります。
スキーム、ホスト名、ポートがハイライト表示された URL の例。これらを組み合わせると、オリジンが形成されます。

たとえば、developer.chrome.com から developer.chrome.com/blog に移動するときに、クロスドキュメント ビューの切り替えを行うことができます。これらは同じオリジンであるためです。developer.chrome.com から www.chrome.com に移動する場合、クロスオリジンで同じサイトであるため、このトランジションは使用できません。


クロスドキュメント ビューの切り替えはオプトイン

2 つのドキュメント間でクロスドキュメント ビューの切り替えを行うには、両方の参加ページでこの切り替えを許可する必要があります。これは、CSS の @view-transition @ 規則で行います。

@view-transition at-rule で、navigation ディスクリプタを auto に設定して、クロスドキュメントの同一オリジン ナビゲーションのビュー遷移を有効にします。

@view-transition {
  navigation: auto;
}

navigation ディスクリプタを auto に設定すると、次の NavigationType でビュー遷移を許可することになります。

  • traverse
  • push または replace(ブラウザの UI メカニズムを通じてユーザーが有効化を開始していない場合)。

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

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

複数ドキュメントにわたるビューの切り替えのデモ

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

Stack Navigator デモの録画。Chrome 126 以降が必要です。

ドキュメント間のビューの切り替えをカスタマイズする

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

これらの機能は View Transition API 仕様自体の一部ではありませんが、View Transition API と組み合わせて使用するように設計されています。

pageswap イベントと pagereveal イベント

Browser Support

  • Chrome: 124.
  • Edge: 124.
  • Firefox: not supported.
  • Safari: 18.2.

Source

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

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

  • pageswap イベントは、ページの最後のフレームがレンダリングされる前に発生します。これを使用して、古いスナップショットが取得される直前に、送信ページに最後の変更を加えることができます。
  • pagereveal イベントは、ページが初期化または再アクティブ化された後、最初のレンダリングの機会の前に発生します。これを使用すると、新しいスナップショットが取得される前に新しいページをカスタマイズできます。

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

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 オブジェクトは、2 つの異なるオブジェクトです。また、さまざまな Promise の処理も異なります。

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

Browser Support

  • Chrome: 123.
  • Edge: 123.
  • Firefox: 147.
  • Safari: 26.2.

Source

pageswap イベントと pagereveal イベントの両方で、古いページと新しいページの URL に基づいてアクションを実行することもできます。

たとえば、MPA Stack Navigator では、使用するアニメーションの種類はナビゲーション パスによって異なります。

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

これを行うには、ナビゲーションに関する情報が必要です。pageswap の場合はナビゲーションがまもなく発生し、pagereveal の場合はナビゲーションが発生したばかりです。

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

アクティブ化されたページでは、navigation.activation を通じてこのオブジェクトにアクセスできます。pageswap イベントでは、e.activation を介してアクセスできます。

pageswap イベントと pagereveal イベントの NavigationActivation 情報を使用して、ビューの切り替えに参加する必要がある要素の view-transition-name 値を設定するこちらのプロファイルのデモをご覧ください。

そうすると、リスト内のすべてのアイテムに view-transition-name を事前に装飾する必要がなくなります。代わりに、JavaScript を使用して必要な要素に対してのみ、ジャストインタイムで実行されます。

プロファイル デモの録画。
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);
    }
  }
});

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

Browser Support

  • Chrome: 124.
  • Edge: 124.
  • Firefox: not supported.
  • Safari: not supported.

場合によっては、特定の要素が新しい DOM に存在するまで、ページの初回レンダリングを遅らせたいことがあります。これにより、点滅を回避し、アニメーションの対象となる状態を安定させることができます。

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

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

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

レンダリング ブロックを全面的に導入する前に、増分レンダリングはウェブの基本的な側面であることを認識しておいてください。レンダリング ブロックを選択する際は注意が必要です。レンダリングのブロックの影響は、ケースバイケースで評価する必要があります。デフォルトでは、ウェブに関する主な指標への影響を測定することで、ユーザーへの影響を積極的に測定し、評価できる場合を除き、blocking=render の使用は避けてください。


クロスドキュメント ビュー遷移で遷移タイプを表示する

クロスドキュメント ビュー遷移では、ビュー遷移タイプもサポートされており、アニメーションとキャプチャされる要素をカスタマイズできます。

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

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

@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 などの機能と相性が良いです。

デモ

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

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

使用する遷移タイプは、pagereveal イベントと pageswap イベントで、遷移元 URL と遷移先 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 バグを報告してください。