問題
ウェブサイトでは、多くの場合、オブジェクトの複数のビュー(商品のリストや各商品の詳細ページなど)が提供されます。2 つのビューを切り替える際に、つながりを感じられないことがよくあります。ユーザーが商品のサムネイルをクリックして詳細を表示すると、サムネイルが消え、画面の別の場所に新しい大きな画像がすぐに表示されます。この連続性の欠如により、ユーザーは要素間の関係を追跡しにくくなります。
ソリューション
View Transitions API を使用すると、遷移前と遷移後の異なる状態に存在する要素ペアを指定できます。シングルページ ナビゲーション(SPA)で document.startViewTransition() を使用して遷移をトリガーすると、ブラウザは共有の一意の view-transition-name によってこれらの共有要素を識別します。次に、位置、サイズ、スタイルの違いを自動的に計算し、古い状態から新しい状態にスムーズにアニメーション化します。この遷移は、z-index 値が高い要素よりも上の最上位レイヤで行われます。
実装ガイド
ステップ 1: 状態の変更を startViewTransition でラップする
シングルページ アプリケーション(SPA)または単純な状態変更の場合は、DOM を更新するロジックを document.startViewTransition でラップします。ブラウザは、現在の状態のスナップショットをキャプチャし、更新を実行してから、新しい状態をキャプチャします。
function navigate(view) {
// MANDATORY: Wrap the update in startViewTransition
document.startViewTransition(() => updateDOM(view));
}
ステップ 2: 共有遷移名を割り当てる
view-transition-name CSS プロパティを使用して、モーフィングする要素をブラウザに指示します。名前は任意です(none を除く)。必須: 指定された view-transition-name の前後に 1 つ以下の要素が必要です。指定された view-transition-name を持つ要素が 2 つ以上ある場合、DOM は遷移なしで直ちに新しい状態に更新されます。
複数の view-transition-name を使用して、複数の要素ペアをモーフィングできます。たとえば、商品画像とタイトルを別々の遷移で遷移させたい場合があります。
リスト表示には複数のアイテムがあるため、すべてのアイテムに同じ view-transition-name を指定することはできません。SPA では、この問題を 2 つの方法で解決できます。
動的な詳細ページ: リストページの各アイテムに一意の
view-transition-nameを割り当て、リストアイテムが選択されたときに、その名前を詳細ページの対応する要素に動的に適用します。/* In the list view, give each */ #product-1 { view-transition-name: p1 } #product-2 { view-transition-name: p2 } #product-3 { view-transition-name: p3 }function updateDOM(clickedTransitionName){ const hero = document.getElementById("hero"); hero.style.viewTransitionName = clickedTransitionName; }動的リストアイテム: 詳細ページの要素に
view-transition-nameを割り当て、その名前をリストページで選択されたアイテムに適用します。リストページに戻るときに、リストページのアイテムからview-transition-nameを削除します。
詳細ページの #hero 要素と、リストページで選択された .thumbnail 要素は、view-transition-name を共有します。
#hero{
view-transition-name: hero;
}
.thumbnail.selected {
view-transition-name: hero;
}
サムネイルがクリックされたら、.selected クラス セレクタを使用して view-transition-name を割り当て、リスト表示を準備する必要があります。また、遷移を開始する前に DOM に変更を加える必要があります。
次に、document.startViewTransition() を呼び出して変更を適用し、ページを詳細ビューからリスト表示に移行します。
リスト表示に戻ったら、次のナビゲーションでエラーが発生しないように、ビュー遷移クラスをクリーンアップする必要があります。このクリーンアップは、遷移の finished promise が解決された後に実行できます。
// Function called when a thumbnail is clicked
function goFromListToDetail(e){
e.currentTarget.classList.add("selected");
const hero = document.getElementById("hero");
const bgColor = getComputedStyle(e.currentTarget).backgroundColor;
hero.style.background = bgColor;
// Trigger the transition, checking for support
if (!document.startViewTransition) {
document.body.classList.add("detail");
// MANDATORY Accessibility Routing: Route focus to the newly revealed heading to announce context and preserve logical tab flow
document.getElementById("detail-heading")?.focus();
return; // MANDATORY: End function execution if view transitions are not supported.
}
const transition = document.startViewTransition(() => {
document.body.classList.add("detail");
});
// MANDATORY Accessibility Routing: Route focus after the view transition resolves
transition.finished.finally(() => {
document.getElementById("detail-heading")?.focus();
});
}
// Function called when navigating from detail back to list view
function goFromDetailToList() {
if (!document.startViewTransition) {
document.body.classList.remove("detail");
document.getElementById("list-heading")?.focus();
return;
}
const transition = document.startViewTransition(() => {
document.body.classList.remove("detail");
});
// Clean up the list view and route focus
transition.finished.finally(() => {
// Route focus back to list view
document.getElementById("list-heading")?.focus();
// Remove selected classList to remove view-transition-names
document.querySelectorAll(".selected").forEach(
(element) => {
element.classList.remove("selected");
},
);
});
}
どの方法を選択するかは、ユースケースによって異なります。動的リストアイテムでは、CSS の繰り返しは少なくなりますが、JavaScript の手動クリーンアップが増えます。
ステップ 3: アスペクト比の「引き伸ばし」を修正する
デフォルトでは、ブラウザは、両方に合わせて拡大されたグループ内で、古いスナップショットと新しいスナップショットをクロスフェードします。テキストを遷移させる場合は、遷移する要素のアスペクト比が安定するように、古いビューと新しいビューの両方でテキスト要素の幅を fit-content に設定します。
#list-page .title {
width: fit-content;
}
#detail-page #title {
width: fit-content;
}
アスペクト比が変化する要素を遷移させる場合は、古い疑似要素と新しい疑似要素の高さを ::view-transition-pair() 疑似要素の 100% に設定する必要があります。
::view-transition-old(hero),
::view-transition-new(hero){
height: 100%;
}
疑似要素はライブ要素のスナップショットであるため、object-fit 宣言と object-position 宣言を使用して、遷移効果をより細かく制御することもできます。
ベスト プラクティス
- 遷移を多く指定しすぎないでください。共有要素は、ユーザーが積極的に追跡しているプライマリ コンテンツ(ヒーロー画像、見出しなど)にのみ使用します。
- 推奨: 移行が完了したら、一時的な
view-transition-name値を削除して、今後の移行に副作用を与えないようにします。 - アクティブなアニメーションを含む要素を遷移させないでください。ビュートランジションはスナップショットに対して動作するため、ビュートランジション中はアニメーションが一時停止しているように見えます。
prefers-reduced-motionメディアクエリを使用して、動きを抑えるユーザー設定を尊重する。- 必須のアクセシビリティ ルーティング: ビュー遷移はページ レイアウトを動的に変更しますが、プログラムによるフォーカスは管理しません。遷移中に非表示または削除された要素にフォーカスが残っていると、フォーカスが失われ、キーボードや支援技術のユーザーにコンテキストが伝わらなくなります。DOM の更新直後、またはビュー遷移の
finishedPromise が解決されたときに、プログラムでフォーカスを更新されたページ見出しまたはビューコンテナに移動します(tabindex="-1"を使用)。
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
代替戦略
View Transitions API は、プログレッシブ エンハンスメントを目的として設計されています。サポートしていないブラウザでは、アニメーションなしで DOM の更新がすぐに実行されます。
function navigate(){
if (!document.startViewTransition) {
// Fallback: Just update the DOM
updateDOM();
} else {
document.startViewTransition(() => updateDOM());
}
}