コハム

Webクリエイターのコハムが提供する、Web制作に役立つ情報をまとめたブログです。

爆速でモダンに!MPAでView Transitionsを導入してサイトを劇的に改善する方法

How to implement view transitions in multi-page apps

記事は上記記事を意訳したものです。

※当ブログでの翻訳記事は元サイト様に許可を得て掲載しています。


View Transition APIは、これまでJavaScriptフレームワークでしか実現できなかったページ遷移とステートベースのUI変更を、より広いウェブで可能にします。

マルチページアプリでのView Transitionの実装方法

これには、シングルページアプリケーション(SPA)でのDOM状態間のアニメーションや、マルチページアプリケーション(MPA)でのページ間のナビゲーションアニメーションが含まれます。つまり、大規模なJavaScript依存や複雑さを必要とせずに、あらゆるタイプのウェブサイトでビュー遷移が可能になります。これはユーザーと開発者の両方にとって大きな進歩です!潜在的にはゲームチェンジャーとなり得ます。

この記事では、MPAでのビュー遷移に焦点を当てます。これはCSS View Transitions Module Level 2仕様で定義されており、クロスドキュメントビュー遷移と呼ばれています。クールなのは、基本的な実装がJavaScriptなしで可能で、宣言的なCSSだけで動作させることができることです!JavaScriptは条件付きロジックを実装したい場合にのみ必要となります。

クロスドキュメントビュー遷移は現在、Chrome 126とSafari 18.2の両方でサポートされています。今すぐにプログレッシブエンハンスメントとしてビュー遷移を使用できます!🙌

なぜビュー遷移を使うべきか?

ビュー遷移はナビゲーション体験を改善します。具体的には:

  • ユーザーが現在のコンテキストを維持するのを助け、変更に注意を引き付け、ユーザーの旅をガイドすることで認知負荷を軽減します
  • 状態/ページの切り替え時の体感的な読み込み遅延を減少させます
  • アニメーションは一般的に洗練された感覚を加え、よりエンゲージメントの高いウェブサイトを実現します

ビュー遷移はどのように機能するか?

すべてのビュー遷移は以下の3つのステップを含みます:

  1. ブラウザが古い状態と新しい状態のスナップショットを取ります。古い状態はスクリーンショットのような静的なスナップショットです。新しい状態はドキュメントのライブな表現です
  2. レンダリングを行わずにDOMが更新されます
  3. 新しい状態への遷移はCSSアニメーションを通じて行われます。デフォルトの遷移はクロスフェードです。古いビューはopacity: 1からopacity: 0にアニメーションし、新しいビューはopacity: 0からopacity: 1にアニメーションします

SPAとMPAでのビュー遷移の主な違いは、遷移がどのようにトリガーされるかです。MPAでは、ビュー遷移は別のページへのナビゲーションによってトリガーされます - これはリンクのクリックやフォームの送信によって発生する可能性があります。URLアドレスバーを使用したナビゲーション、ブックマークのクリック、ページのリロードなどは、ビュー遷移をトリガーしません。

ナビゲーションに時間がかかりすぎる場合、ビュー遷移はスキップされ、エラーとなります。Chromeの制限は4秒です。この文脈でナビゲーションの開始を定義するものが何なのかは不明確です - 最初のバイトがダウンロードされる必要があるのでしょうか?

興味深いことに、1つのページで複数のビュー遷移を持つことができます。DOMの特定のサブツリーに対して遷移をターゲットにすることができ、それらをネストすることも可能です。

ビュー遷移を有効にするためにはいくつかの条件があり、次のセクションで説明します。

基本的なビュー遷移の作成

ウェブサイトでビュー遷移を有効にするには、2つの重要な条件を満たす必要があります:

  1. ソースページとターゲットページは同じオリジンである必要があります:URLは同じスキーム、ドメイン、ポートを持つ必要があります
  2. 両方のページでビュー遷移を有効にする必要があります:これは以下のように@view-transition CSSルールを通じて行われます:
@view-transition {
  navigation: auto;
}

これにより、ページ間でデフォルトのクロスフェードビュー遷移が有効になります。例を見てみましょう。

以下は写真のセットを特徴とするカルーセルのデモです。カルーセルは写真ギャラリーなどのコンテンツセットを循環させることができるコンポーネントです。この例では、効果を強調するためにアニメーションを2秒に遅くしています。

ビュー遷移を使用することで、各ページがアイテムとなるカルーセルを作成できます。次のページと前のページへのリンクと、上記のCSSスニペットだけが必要です。コードでアイテムを操作したり、大量の画像を1つのページに詰め込んだりする必要はありません:

<!-- index.html - 最初のページ -->
<h1>Cape Town</h1>
<img src="cape-town.webp" alt=".."/>
<a href="page2.html" class="next"><img src="/1-carousel/shared/img/arrow-right.svg" /></a>

<!-- page2.html - 2番目のページ -->
<h1>Hong Kong</h1>
<img src="hong-kong.webp" alt=".."/>
<a href="index.html" class="previous"><img src="/1-carousel/shared/img/arrow-left.svg" /></a>
<a href="page3.html" class="next"><img src="/1-carousel/shared/img/arrow-right.svg" /></a>

ビュー遷移を有効にするには、他にも3つの条件があります。デフォルトでこれらの条件を満たしている可能性が高いです。

仕様の細かい部分では、以下の条件をすべて満たす必要があります:

  • 両方のページでビュー遷移を有効にする必要があります
  • ソースページとターゲットページは同じオリジンである必要があります
  • ナビゲーション全体を通してページが表示されている必要があります
  • ナビゲーションはページによって開始される必要があります(リンクのクリック、フォームの送信、または履歴の前後移動など)
  • ナビゲーションにクロスオリジンのリダイレクトを含めることはできません

ビュー遷移のアニメーションのカスタマイズ

擬似要素を通じてアニメーションをカスタマイズできます:

  • ::view-transition-group()は特定のビュー遷移を参照するために使用されます
  • ::view-transition-old()はソースビュー(送出遷移)を参照するために使用されます
  • ::view-transition-new()はターゲットビュー(受入遷移)を参照するために使用されます

これらの擬似要素それぞれに対して、参照したいビュー遷移の名前を引数として提供します。ページのビュー遷移のデフォルト名は、:root要素に適用されるため「root」です。

両方のページで以下のようにしてアニメーションの持続時間を変更できます:

::view-transition-group(root) {
  animation-duration: 3s;
}

異なる遷移効果を作成するために、ソース(古い)ビューとターゲット(新しい)ビューのアニメーションを個別に設定できます。

例えば、縮小アニメーションのデモを作ってみましょう。ソースページを縮小して視界から消し、ターゲットページを拡大して表示させます:

@keyframes shrink {
  to {
    scale: 0;
  }
}

::view-transition-old(root) {
  animation: shrink 1s;
}

::view-transition-new(root) {
  animation: shrink 1s;
  animation-direction: reverse;
}

ターゲットビュー遷移にanimation-direction: reverseを設定していることに注目してください。これにより「縮小」アニメーションが拡大に変わります!

反対の動作を設定することで、心地よい対称的な効果が生まれます。ただし、これは必須ではありません - 各アニメーションを独立したものとして扱うこともできます。お好みの効果を自由に作成できます!

クロスドキュメントビュー遷移の場合、これらの擬似要素はターゲットページでのみ利用可能です。一方向のみのビュー遷移を作成する場合は、これを忘れないようにしましょう。

それでは、JavaScriptを導入して、ビュー遷移でできることをさらに見ていきましょう!

クロスドキュメントビュー遷移のカスタマイズ

これまで、クロスドキュメントビュー遷移を有効にし、CSSでアニメーションをカスタマイズできることを示してきました。これは強力ですが、さらに多くのことを行いたい場合は、JavaScriptが必要です。

View Transition APIはすべてのニーズをカバーしているわけではありません。それと組み合わせて使用するように設計された補完的なウェブ機能があります。これらは以下のカテゴリーに分類されます:

  1. ライフサイクルイベント:

    • pageswappagerevealイベントにより、ビュー遷移の条件付きアクションを指定できます
    • pageswapイベントはソースページがアンロードされる前に発火します
    • pagerevealはターゲットページのレンダリング前に発火します
  2. ナビゲーション情報:

    • ブラウザは同一オリジンのナビゲーションに関する情報を保持するNavigationActivationオブジェクトを公開するようになりました
    • これにより、開発者は異なるURLに基づいて異なるアニメーション/アクションを実行したい場合に、この情報を自分で追跡する手間が省けます
  3. 宣言的レンダーブロッキング:

    • 場合によっては、特定の要素が存在するまでターゲットページのレンダリングを保留したい場合があります
    • これにより、アニメーション先の状態が安定していることを保証します

これらの機能について、いくつかの例を使って詳しく見ていきましょう。

pageswapとpagerevealイベントの使用

pageswappagerevealイベントを使用することで、ビュー遷移に条件付きロジックを実装する機会が得られます。

pageswapイベントは、ソースページがアンロードされ、ターゲットページと交換される直前の最後の瞬間に発火します。このイベントは以下のような用途に使えます: - ビュー遷移が行われようとしているかどうかを確認する - タイプを使用してカスタマイズする - キャプチャされた要素に最後の変更を加える - ビュー遷移を完全にスキップする

pagerevealイベントは、ターゲットページの最初のフレームを表示する直前に発火します。このイベントは以下のような用途に使えます: - 異なるナビゲーションタイプに基づいて動作する - キャプチャされた要素に最後の変更を加える - アニメーションの準備が整うまで待機する - 遷移をスキップする

これらのイベントでは、ViewTransitionプロパティを使用してViewTransitionオブジェクトにアクセスできます。ViewTransitionオブジェクトはアクティブなビュー遷移を表し、アニメーションが実行される直前や、アニメーションが終了した直後など、遷移が異なる状態に達したときに反応する機能を提供します。

例を見て、これらの概念をつなげてみましょう。

デモ:ユーザーがビュー遷移を無効化/有効化できるようにする

ユーザーがビュー遷移を無効化/有効化できるデモを作成しましょう。カルーセルの右上にチェックボックスを追加します。チェックされている場合、ビュー遷移をスキップします:

HTMLを修正してチェックボックス入力を追加し、これから作成するスクリプトを指すscriptタグを追加する必要があります。このスクリプトタグは<head>内のパーサーブロッキングスクリプトとして追加する必要があります。これはpagerevealイベントが最初のレンダリング機会の前に実行される必要があるためです。つまり、スクリプトはモジュールにはできず、async属性もdefer属性も持てません:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 他の要素は以前と同じ -->

    <!-- スクリプトはここに正確にこの形で配置する必要があります -->
    <script src="script.js"></script>
  </head>

  <body>
    <label>スキップ?<input type="checkbox" id="skip" /></label>

    <!-- 他の要素は以前と同じ -->
  </body>
</html>

スクリプトでは、pageswappagerevealイベントのイベントハンドラーを追加します。pageswapイベントハンドラーでは、チェックボックスの値(trueまたはfalse)をセッションストレージにskip変数として書き込みます:

/* script.js */

// 古いページでストレージに書き込む
window.addEventListener("pageswap", (event) => {
  if (event.viewTransition) {
    let skipCheckbox = document.querySelector("#skip");
    sessionStorage.setItem("skip", skipCheckbox.checked);
  }
});

// 新しいページでストレージから読み込む
window.addEventListener("pagereveal", (event) => {
  if (event.viewTransition) {
    let skip = sessionStorage.getItem("skip");
    let skipCheckbox = document.querySelector("#skip");

    if (skip === "true") {
      event.viewTransition.skipTransition();
      skipCheckbox.checked = true;
    } else {
      skipCheckbox.checked = false;
    }
  }
});

ViewTransitionオブジェクトを参照して、値を保存するかどうかを判断していることに注目してください。ViewTransitionオブジェクトは、ビュー遷移が行われていない場合はnullです。したがって、このチェックはビュー遷移が行われているときにtrueを返します。

pagerevealイベントハンドラーでは、セッションストレージからskip変数の値を読み取ります。skipの値が"true"の場合(セッションストレージはすべての値を文字列として保存します)、ViewTransition.skipTransition()関数を呼び出してビュー遷移をスキップします。

ターゲットページでチェックボックスの状態を維持するために、pagerevealでセッションストレージの値を使用します。これにより、ページのナビゲーション間でチェックボックスの状態が保持されます。HTTPはステートレスプロトコルであり、明示的に指示しない限り前のページに関するすべての情報を忘れてしまうことを覚えておいてください!

セッションストレージに慣れていない場合は、ChromeのDevToolsでセッションストレージを確認できます。アプリケーションタブで見つけることができます。サイドバーのStorageカテゴリの下に、Session storageアイテムが表示されます。クリックすると、ウェブサイトのオリジン(例:http://localhost:3000)が表示されます。クリックすると、保存されているすべての値が表示されます。

ナビゲーションアクティベーション情報

pageswappagerevealイベントでは、行われているナビゲーションに基づいてアクションを実行できます。この情報はNavigationActivationオブジェクトを通じて利用可能です。このオブジェクトは、使用されているナビゲーションタイプ、ソースページのナビゲーション履歴エントリ、およびターゲットページのナビゲーション履歴エントリを公開します。これらのナビゲーション履歴エントリを通じて、各ページのURLを取得できます。執筆時点では、ChromeのみがNavigationActivationオブジェクトをサポートしています。

デモ:カルーセルのスライドアニメーション

カルーセルにスライドアニメーションを追加するデモを作成しましょう。以下のような動作を実現します:

  • 「次へ」リンクをクリックすると、ソースビューを左にスライドアウトし、ターゲットビューを右からスライドインします
  • 「前へ」リンクをクリックすると、ソースビューを右にスライドアウトし、ターゲットビューを左からスライドインします

このシナリオでは、ビュー遷移タイプを使用できます。アクティブなビュー遷移に1つ以上のタイプを、ViewTransition.typesプロパティで利用可能なSetオブジェクトを通じて割り当てることができます。この例では、シーケンスの上位ページに遷移する場合は「next」タイプを割り当て、下位ページに遷移する場合は「previous」タイプを割り当てます。

各タイプはCSSで参照して、異なるアニメーションを割り当てることができます:

ではどのようにタイプを決定するのでしょうか?

タイプの決定は開発者次第です!

この場合、ソースページとターゲットページのURLを調べて、順序を識別します。NavigationActivationオブジェクトからソースページとターゲットページのURLを取得できます。このオブジェクトには、ソースページを履歴エントリとして表すfrom属性と、ターゲットページを履歴エントリとして表すentry属性があります。

ファイルの命名規則が順序を示しているため、これを使用して各ページのインデックスを識別できます。順序は以下の通りです:

  1. index.html
  2. page2.html
  3. page3.html

コードでは、determineTransitionType関数がソースページとターゲットページのインデックスを比較して、「previous」タイプか「next」タイプかを判断します:

window.addEventListener("pageswap", async (e) => {
  if (e.viewTransition) {
    let transitionType = determineTransitionType(
      e.activation.from.url,
      e.activation.entry.url
    );

    e.viewTransition.types.add(transitionType);
  }
});

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    // pagerevealはNavigationActivationオブジェクトを公開しないため、グローバルオブジェクトから取得する必要があります
    let transitionType = determineTransitionType(
      navigation.activation.from.url,
      navigation.activation.entry.url
    );

    e.viewTransition.types.add(transitionType);
  }
});

function determineTransitionType(sourceURL, targetURL) {
  const sourcePageIndex = getIndex(sourceURL);
  const targetPageIndex = getIndex(targetURL);

  if (sourcePageIndex > targetPageIndex) {
    return "previous";
  } else if (sourcePageIndex < targetPageIndex) {
    return "next";
  }

  return "unknown";
}

function getIndex(url) {
  let index = -1;
  let filename = new URL(url).pathname.split("/").pop();

  if (filename === "index.html") {
    index = 1;
  }

  // ファイル名から数字を抽出
  let numberMatches = /\d+/g.exec(filename);
  if (numberMatches && numberMatches.length === 1) {
    index = numberMatches[0];
  }

  return index;
}

スタイルシートでは、必要な4つのアニメーションを指定します。名前は文字通りの意味になっています:

@keyframes slide-in-from-left {
  from {
    translate: -100vw 0;
  }
}

@keyframes slide-in-from-right {
  from {
    translate: 100vw 0;
  }
}

@keyframes slide-out-to-left {
  to {
    translate: -100vw 0;
  }
}

@keyframes slide-out-to-right {
  to {
    translate: 100vw 0;
  }
}

:active-view-transition-type()擬似クラスを使用して、アニメーションをビュー遷移タイプに関連付け、引数としてタイプを提供します。

各タイプに対して、::view-transition-old()でソースページのアニメーションを、::view-transition-new()でターゲットページのアニメーションを指定します:

::view-transition-group(root) {
  animation-duration: 400ms;
}

html:active-view-transition-type(next) {
  &::view-transition-old(root) {
    animation-name: slide-out-to-left;
  }

  &::view-transition-new(root) {
    animation-name: slide-in-from-right;
  }
}

html:active-view-transition-type(previous) {
  &::view-transition-old(root) {
    animation-name: slide-out-to-right;
  }

  &::view-transition-new(root) {
    animation-name: slide-in-from-left;
  }
}

これらすべてを理解するのには少し時間がかかります!しかし、慣れてくると、クロスドキュメントビュー遷移のために多様なアニメーションを実現できるようになります。これは刺激的な展望です!

宣言的レンダーブロッキング

場合によっては、特定の要素が存在するまでターゲットページのレンダリングを保留したい場合があります。これにより、アニメーション先の状態が安定していることを保証します:

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

これにより、要素がDOMに存在することは保証されますが、コンテンツが完全に読み込まれるまでは待機しません。この機能を画像やビデオなど、読み込みに時間がかかる可能性のあるものと一緒に使用する場合は、このことを考慮に入れる必要があります。

この機能は賢明に使用してください。一般的に、レンダリングをブロックすることは避けたいものです!クロスドキュメントビュー遷移の探索では、これのユースケースは見つかりませんでしたが、その存在を知っておくことは重要です!

View Transition APIのブラウザサポート

ビュー遷移に関するブラウザサポートは、ChromeとSafariが関連APIの大部分をカバーしており、強力です:

機能 Chrome Safari
クロスドキュメントビュー遷移 v126+ v18.2+
ビュー遷移タイプ v125+ v18.2+
PageRevealEvent v123+ v18.2+
PageSwapEvent v124+ v18.2+
NavigationActivation インターフェース v123+
レンダーブロッキング v124+
ネストされたビュー遷移グループ #enable-experimental-web-platform-featuresフラグで有効化 v18.2+
自動ビュー遷移命名 #enable-experimental-web-platform-featuresフラグの背後 v18.2+

View Transition APIのアクセシビリティに関する考慮事項

アニメーションがどれだけクールに見えても、前庭器官障害のある人々に問題を引き起こす可能性があります。そのようなユーザーのために、アニメーションを遅くしたり、より控えめなアニメーションを選択したり、アニメーションを完全に停止したりすることができます。これにはprefers-reduced-motionメディアクエリを使用できます。

最も簡単な方法は、モーション低減の設定を持たない人々に対してのみビュー遷移を有効にすることです。この設定を持つ人々に対しては、デフォルトで無効になります:

/* モーション低減を設定していない人々に対してのみビュー遷移を有効にする */
@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

開発環境に関する推奨事項

クロスドキュメントビュー遷移を扱う際は、ホットリロード開発サーバーを使用している場合は注意が必要です。ページがキャッシュされているか、ページが完全にリロードされていない場合、変更が反映されないことがあります。

キャッシュが行われないようにする最も簡単な方法は、開発者ツールを開き、ネットワークタブで「Disable cache」チェックボックスを選択することです。

また、console.log()などを使用してデバッグすることに慣れている場合、2つのページにまたがって作業する場合には効果的ではありません。すべてのナビゲーションでコンソールログはクリアされます。これが好みのデバッグ方法である場合は、sessionStorageを使用してログを記録する方が良いでしょう。

デモ

この記事で紹介したすべてのデモは、GitHubリポジトリで見つけることができます。Chrome DevRelチームが準備したデモもいくつか含まれています。

紹介したデモのライブページへのリンクは以下の通りです:

ビュー遷移を見るには、ページ間のナビゲーションが4秒以内である必要があります。

結論

あらゆるウェブサイトにページ遷移とステートベースのUI変更を追加する機能は、大きな前進です。大規模なJavaScript依存やフレームワークの複雑さなしにビュー遷移を適用できることは、ユーザーと開発者の両方にとって良いことです!いくつかの簡単なCSSで始めることができます。ビュー遷移に条件付きロジックが必要な場合は、JavaScriptコードを書く必要があります。

一般的に、この機能は印象的です。ただし、クロスドキュメントビュー遷移の使用に関する側面を理解するのに苦労したことは認めなければなりません。View Transition APIとNavigationActivationなどの補完的なAPIの関係は、読んだ説明からは明らかではありませんでした。これらの理解の障壁を乗り越えれば、適度な長さのJavaScriptコードで効果的なビュー遷移を書くことができます。

ビュー遷移に関連するAPIのブラウザサポートは、ChromeとSafariが大部分をカバーしており、強力です。いずれにせよ、ビュー遷移をプログレッシブエンハンスメントとして使用できます。新しいウェブ機能であることを念頭に置き、いくつかの問題に遭遇する可能性があることを意識してください。

また、クロスページビュー遷移には高速に読み込まれるページが必要であることを理解することも重要です。ナビゲーションが4秒を超えると、Chromeはビュー遷移を中止します。将来のナビゲーションのパフォーマンスを向上させるために使用できる補完的なウェブ機能は、プリレンダリングです。Speculation Rules APIは、将来のナビゲーションのパフォーマンスを向上させるように設計されています。これらの機能は、より高速で機能的なウェブを指し示していますが、そのメリットを実現するには、人々が新しい方法でウェブサイトを構築する必要があります。

ビュー遷移の機能も拡大しています。ネストされたビュー遷移が最近追加され、いくつかの実験的な追加機能が検討されています。Chrome DevRelチームは、ナビゲーションの条件にさらなるオプションを追加することを望んでおり、クロスオリジンビュー遷移も許可する可能性があります!

クロスドキュメントビュー遷移を試してみてください!

©コハム