タグアーカイブ View Transitions

View Transitions、大量要素スケーリングにview-transition-classが効く

View Transitions、大量要素スケーリングにview-transition-classが効く

クロスドキュメントビュートランジション(View Transitions)は、ページ間の遷移をアプリのように滑らかにする強力なAPIだ。しかし本番環境で数十、数百の要素を扱おうとすると、途端にスケーリングの問題に直面する。1つのヒーロー画像を動かすデモは簡単だが、48枚の商品カードを個別に遷移させるとなると話が違う。

本記事では、view-transition-classと動的な名前付けの手法を用いて、大量要素を効率よく扱う方法を解説する。CSS-Tricksで公開された連載Part 2の内容を基に、実践的なパターンとアクセシビリティへの配慮までカバーする。

view-transition-classがスケーリングの鍵

view-transition-classがスケーリングの鍵

多くのチュートリアルでは、1つの要素にview-transition-name: heroを付与し、ページ間でマッチさせる。しかし実際のプロダクトグリッドでは、48枚のカードには48の一意な名前が必要になる。CSSでこれに対応しようとすると、次のような悪夢が待っている。

::view-transition-group(card-1),
::view-transition-group(card-2),
::view-transition-group(card-3),
::view-transition-group(card-4),
::view-transition-group(card-5),
::view-transition-group(card-6),
::view-transition-group(card-7),
::view-transition-group(card-8)
/* ... さらに92個続く */ {
  animation-duration: 0.35s;
  animation-timing-function: ease-out;
}

この方法は、要素数が増えるほど管理不能になる。100個の商品があれば100個のセレクタを書かなければならず、保守は事実上不可能だ。

名前とクラスの決定的な違い

ここで重要になるのが、view-transition-nameview-transition-classの使い分けだ。両者は似ているようで役割がまったく異なる。

  • nameは「同一性」を表す。ページAのサムネールとページBのヒーロー画像が「同じもの」だとブラウザに伝える。nameはページ内で一意でなければならない。重複するとトランジションは破棄される。
  • classは「スタイルのフック」だ。50の要素がview-transition-class: cardを持てば、1つのCSSルールでそれらすべてのアニメーションを制御できる。通常のCSSクラスと同じ考え方で、特定の要素を指すものではなく「こう見せたい」をグループ化する。

データベースにたとえるなら、nameが主キー、classがカテゴリ列に相当する。主キーは一意に1行を特定し、カテゴリ列はまとめてクエリをかけるために使う。

クラスを使った共通スタイルの記述

実際のCSSはこうなる。6枚のカードに6つのユニークなnameを与えつつ、アニメーションのルールはたった3つで済む。

::view-transition-group(*.card) {
  animation-duration: 0.35s;
  animation-timing-function: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}

::view-transition-old(*.card),
::view-transition-new(*.card) {
  object-fit: cover;
}

セレクタの*.cardは「view-transition-classがcardであるすべてのビュートランジショングループ」を意味する。アスタリスクはnameのワイルドカードで、classにマッチする。これでカードが60枚でも600枚でもCSSは変わらない。

従来手法(各カードに個別セレクタ)
48個のカードそれぞれに固有のルールを記述する必要がある
::view-transition-group(card-1), ::view-transition-group(card-2), …
※ 100個を超えると事実上管理不能
view-transition-class を使った改善後
共通クラスに1つのルールを書くだけ
::view-transition-group(*.card) { animation-duration: 0.35s; }
※ 何枚でもCSSは変わらない
■ 要素数に依存せず、わずか数行で全カードのアニメーションを制御できる

このように、view-transition-classは大量要素のビュートランジションにおける、スケーリングの本質的な解決策だ。CSSのみで記述する理想形であるident("card-" sibling-index())のような自動生成はまだブラウザに実装されていないが、クラスを使えば十分なスケールを得られる。

動的名前付けでパフォーマンスを最適化

動的名前付けでパフォーマンスを最適化

view-transition-classでスタイルのスケーリングは解決した。しかし、nameをページロード時にすべて付与してしまうと、別の問題が発生する。ユーザーが1枚のカードをクリックするだけでも、ページ上の全カード(48枚)のスナップショットが撮られ、疑似要素ツリーが構築されてしまうのだ。これは余計なコストであり、特にミドルレンジのモバイル端末ではトランジションのカクつきやスキップを引き起こす。

pageswapとpagerevealのライフサイクル

正しいアプローチは、nameを「ジャストインタイム」で付与することだ。ユーザーが操作したその瞬間にだけnameを設定し、遷移が終われば削除する。これにより、実際に遷移する要素だけがキャプチャされ、無駄な処理が発生しない。

流れはこうだ。

  • ユーザーが一覧ページでカードをクリックする。
  • ブラウザがナビゲーションを開始し、旧ページでpageswapイベントが発火する。
  • pageswapハンドラがクリックされたカードを特定し、view-transition-name: product-42を動的にセットする。
  • ブラウザがその要素のスナップショットを撮る。
  • 新ページが読み込まれ、pagerevealイベントが発火する。
  • pagerevealハンドラがURLからIDを読み取り、ヒーロー要素に一致するnameを割り当てる。
  • ブラウザが新旧のスナップショットをマッチさせ、モーフィングアニメーションを再生。
  • トランジションが完了したら、viewTransition.finishedのPromise解決後にnameをクリアする。

この一連の流れで、名付けられるのはたった1つの要素だけだ。48枚のカードのうち47枚は何も関与せず、無駄なスナップショットはゼロになる。

商品一覧ページ(クリック前)
カード42 data-id=”product-42″(name未設定)
ページロード時は全カードが匿名
↓ クリック
pageswap イベント発火
JavaScriptが view-transition-name: product-42 を動的に付与
↓ スナップショット → 遷移
商品詳細ページ
ヒーロー画像 pagereveal で view-transition-name: product-42 を設定
名前が一致し、モーフィング開始
■ クリックされた1要素だけがトランジションに参加し、残りは無視される

このパターンは、Astroのtransition:nameディレクティブやNuxtのビュートランジションサポートが内部的に行っていることと本質的に同じだ。フレームワークが抽象化している処理を、pageswappagerevealで直接制御していると考えればよい。

名前のクリーンアップが重要な理由

トランジション完了後にnameを削除するステップは、単なるお片付けではない。もしユーザーが一覧ページに戻り、別のカードをクリックした場合、古いnameが残っていると重複によるエラー(トランジションが即時破棄される)か、誤った要素とのマッチングが起きる。必ずviewTransition.finishedの解決後にnameをクリアすること。

実践的なパターン集

実践的なパターン集

商品グリッド以外にも、いくつかの典型的なパターンが存在する。実際のサイトで遭遇する状況に合わせて応用できる。

アスペクト比混合のフォトギャラリー

サムネールと拡大画像でアスペクト比が異なるギャラリーは、object-fit: coverで歪みを防ぎつつ、クラスで統一的に制御する。ポイントは、view-transition-name<img>自身に付与し、カードの枠やキャプションを含めないことだ。画像だけをモーフィングさせ、背景や枠は別のトランジションとして扱う。

::view-transition-group(*.gallery-item) {
  animation-duration: 0.5s;
  animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}

::view-transition-old(*.gallery-item),
::view-transition-new(*.gallery-item) {
  object-fit: cover;
  overflow: hidden;
}

/* ライトボックス背景は別クラスでフェード */
::view-transition-group(*.lightbox-bg) {
  animation-duration: 0.3s;
}

ライトボックスの暗いオーバーレイには、別のnameとclassを与え、独立したフェードインアニメーションを適用する。画像のモーフィングと背景のフェードが並行して走り、洗練された印象になる。

タブやセクションの切り替え

ダッシュボードタブやマルチステップフォームなど、同一ページ内でのセクション遷移にも同じ手法が使える。固定ヘッダーにはanimation-duration: 0sを指定して「動かない」ようにし、コンテンツだけがスライドする感覚を出す。

::view-transition-group(*.persistent) {
  animation-duration: 0s; /* 動かさない */
}

::view-transition-group(*.tab-content) {
  animation-duration: 0.25s;
}

::view-transition-old(*.tab-content) {
  animation: slide-out-left 0.25s ease-in;
}

::view-transition-new(*.tab-content) {
  animation: slide-in-right 0.25s ease-out;
}

永続的な要素にアニメーションをかけないことで、UI全体に安定感が生まれる。

無限スクロールと動的コンテンツ

無限スクロールで後からDOMに追加された要素にも、特別な対応は不要だ。pageswapハンドラはナビゲーション発生時にDOMをクエリする。要素がその時点で存在していれば、問題なくnameを割り当てられる。唯一注意すべきは、data-idなどマッチングに使う識別子が動的に追加されたバッチ間でも一意であることだ。APIが返すIDを利用していれば問題ない。

アクセシビリティとprefers-reduced-motion

アクセシビリティとprefers-reduced-motion

アニメーションは、前庭障害を持つユーザーに吐き気やめまい、片頭痛を引き起こす可能性がある。prefers-reduced-motionメディアクエリは、OSレベルで「動きを減らしてほしい」と設定しているユーザーを検出する。ビュートランジションを導入するなら、この対応は必須だ。

@view-transition {
  navigation: auto;
}

/* アニメーションのカスタマイズはすべてこのメディアクエリ内に */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(*.card) {
    animation-duration: 0.35s;
    animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  }

  ::view-transition-old(*.card),
  ::view-transition-new(*.card) {
    object-fit: cover;
  }

  ::view-transition-old(root) {
    animation: fade-out 0.2s ease-in;
  }

  ::view-transition-new(root) {
    animation: fade-in 0.3s ease-out;
  }
}

/* 動きを減らす設定の場合は0秒で即座に切り替え */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation-duration: 0s !important;
  }
}

根本的に安全を取るなら、@view-transitionの宣言自体をprefers-reduced-motion: no-preferenceで囲み、トランジションを完全に無効化する方法もある。どちらを選ぶにせよ、アニメーションを無配慮に提供することだけは避けなければならない。

prefers-reduced-motion: no-preference(通常)
カードA → 0.35sのスケール&移動アニメーション
スムーズなモーフィングが再生される
prefers-reduced-motion: reduce(設定ユーザー)
カードA → 0sで即座に切り替え
視覚的な負荷を回避し、安全に遷移する
■ 設定に応じてアニメーションの有無を切り替えるのが基本

なお、prefers-reduced-motion: reduceのユーザー向けに、完全に0秒にする代わりに短いクロスフェード(0.15秒)を提供する手法もある。動きそのものをゼロにするのが最も安全だが、穏やかなフェードなら許容できるユーザーもいる。ただし、実際にその設定に依存するユーザーでテストするまでは、0秒を選択しておく方が無難だ。

プログレッシブエンハンスメントとブラウザ対応

プログレッシブエンハンスメントとブラウザ対応

ビュートランジションは、プログレッシブエンハンスメントの理想的な例だ。ブラウザが@view-transitionルールを理解しなければ、単に無視され、通常のページ遷移が行われる。何も壊れない。エラーもレイアウトシフトも発生しない。Firefoxがまだサポートしていなくても問題はなく、Safari 18.2以降やChrome、Edgeではフル機能が使える。

唯一、@supports (view-transition-name: none)でガードする価値があるのは、トランジション専用のスタイル(スナップショット品質向上のためのcontain: paintなど)を適用する場合だけだ。それ以外は、古いブラウザでも何もせずにそのまま動く。

この記事のポイント

  • view-transition-nameは一意の識別子、view-transition-classはスタイルをグループ化するフック。クラスを使えば、数百要素でも数行のCSSでアニメーションを統制できる。
  • nameはページロード時に全要素に付与せず、pageswapとpagerevealを使ってクリック時に動的に設定する。これでパフォーマンスが大幅に向上する。
  • トランジション完了後は必ずnameをクリアし、古い名前の衝突を防ぐ。
  • prefers-reduced-motionの対応は必須。すべてのアニメーションカスタマイズをメディアクエリ内に閉じ込め、設定ユーザーには0秒または短いフェードを提供する。
  • ビュートランジションはプログレッシブエンハンスメント。未対応ブラウザでは何も起こらず、通常のページ遷移となる。
クロスドキュメントビュー遷移の三大落とし穴と回避策

クロスドキュメントビュー遷移の三大落とし穴と回避策

クロスドキュメントビュー遷移(Cross-Document View Transitions)は、MPA(マルチページアプリケーション)でありながらSPAのようなスムーズなページ遷移アニメーションを実現するブラウザAPIだ。ReactやAstroといったフレームワークは不要で、HTMLページ間のリンク遷移にブラウザが自動的にアニメーションを付与する。

しかし、この機能の導入にはいくつかの厄介な落とし穴が潜んでいる。CSS-Tricksの記事によれば、著者は実装に丸一日を費やし、何も動作しない状態からデバッグを繰り返したという。ネット上には古い情報や誤解を招くチュートリアルが溢れており、仕様自体も短期間で変更されている。

本記事では、実際の開発現場で遭遇する3つの主要な問題(非推奨metaタグ、4秒タイムアウト、画像の歪み)とその解決策を解説する。加えて、遷移ライフサイクルを制御する2つのイベントについても触れる。

非推奨となったmetaタグの罠

非推奨となったmetaタグの罠

多くの開発者が最初にハマるのが、古いチュートリアルに記載された<meta>タグによるオプトイン方式だ。この方式は既に非推奨であり、現在のブラウザでは完全に無視される。

非推奨(Before)
<meta name=”view-transition” content=”same-origin”>
ブラウザが沈黙し、何も動作しない
推奨(After)
@view-transition { navigation: auto; }
CSSでオプトイン、条件付き制御も可能

この比較が示すように、現在の正しい実装方法はCSSの@規則を使用することだ。Chrome 111でmetaタグが導入された後、Chrome 126前後でCSSベースの方式に置き換えられた。非推奨の警告はDevToolsに表示されず、古いコードは静かに動作しなくなる。

なぜCSS方式に移行したのか

metaタグ方式の最大の欠点は、ページ全体でオンかオフかの二択しかできなかったことだ。CSS方式ではメディアクエリや@supportsと組み合わせて、条件付きのオプトインが可能になる。

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
  }
}

@media (min-width: 768px) {
  @view-transition {
    navigation: auto;
  }
}

このアプローチにより、アニメーションに敏感なユーザーへの配慮や、モバイルデバイスでのパフォーマンス最適化が容易になった。CSSにオプトインが統合されたことで、既存のスタイル管理フローと一貫性を持って扱える点も大きい。

両方のページでオプトインが必須

もう一つの重要なポイントは、遷移を機能させるには遷移元と遷移先の両方のページで@view-transitionが宣言されている必要があることだ。片方だけでは何も起きない。これは意図的な設計で、404ページやログインリダイレクトなど遷移をスキップしたいページを柔軟に制御できる。

なお、navigation: autoはユーザーがリンクをクリックするかブラウザの戻るボタンを押した場合のみ発動する。window.location.hrefによるプログラム的な遷移や、クロスオリジンのリンク、POSTリクエストでは動作しない。この保守的な設計は、決済処理などの重要な操作に意図しないアニメーションが混入するのを防ぐためだ。

4秒タイムアウトが遷移を静かに殺す

4秒タイムアウトが遷移を静かに殺す

ビュー遷移の実装で最もデバッグが難しい問題が、ハードコードされた4秒のタイムアウトだ。新しいページが4秒以内にレンダリング可能な状態に達しないと、遷移アニメーションは何の通知もなくキャンセルされ、通常のページ読み込みのように切り替わる。

タイムアウト発生 4秒経過 ブラウザ 遷移を破棄 エラー表示なし
※TTFB(サーバー応答待ち2秒 + レンダリング2.5秒でアウト)

この問題が厄介なのは、ローカル開発環境ではまず発生しないことだ。devサーバーは80msで応答するため遷移は完璧に動作するが、本番環境でサーバーレス関数のコールドスタートやCDNキャッシュミスが発生すると、最初のクリックで遷移が無効化される。

pagerevealイベントでタイムアウトを捕捉する

タイムアウトの発生を検知するには、pagerevealイベントとviewTransition.finishedプロミスを使用する。以下のコードをページに組み込めば、遷移が失敗した際にコンソールで確認できる。

window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) {
    console.log("ビュー遷移なし");
    return;
  }
  event.viewTransition.finished
    .then(() => console.log("遷移完了 ✅"))
    .catch((err) => {
      console.error("遷移中断", err.name, err.message);
    });
});

このリスナーを早期にセットアップしておけば、本番環境でのデバッグが格段に容易になる。pageswapイベントでも同様に遷移元ページ側でタイムアウトを捕捉可能だ。

実用的な対策

タイムアウト対策の基本はページの読み込み速度改善だが、より実践的なアプローチとしてrel="expect"属性の活用がある。

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

これはブラウザに「#hero要素がDOMに存在するまでページをレンダリング可能と見なさない」と指示するものだ。一見するとパフォーマンスを悪化させるように思えるが、ビュー遷移においては重要なコンテンツが揃ってからスナップショットを取得するため、中途半端な状態での遷移を防げる。

タイムアウトのクロックはナビゲーション開始時からカウントされるため、サーバー応答時間(TTFB)も含まれる点に注意が必要だ。サーバーが2秒かけて応答し、さらに2.5秒かけてレンダリングする場合、個別には遅く感じなくても合計で4.5秒となりタイムアウトに引っかかる。

画像が歪む根本原因と解決策

画像が歪む根本原因と解決策

ビュー遷移の実装で視覚的に最も目立つ問題が、アスペクト比の異なる画像間の遷移で発生する歪みだ。サムネイルからヒーロー画像への遷移で、画像が引き伸ばされて見苦しくなる現象は多くの開発者が経験する。

CSS-Tricksの著者は、この問題の原因を特定するのにかなりの時間を費やしたという。根本的な原因は、ブラウザが遷移中に<img>要素そのものをアニメーションさせるのではなく、古い状態と新しい状態のスクリーンショット(ビットマップ)を取得し、それらを変形させることにある。

変形した状態(Before)
サムネイル
object-fit: cover → 無効化
ビットマップが引き伸ばされる
整形された状態(After)
::view-transition-old(hero-img),
::view-transition-new(hero-img) {
object-fit: cover;
overflow: hidden;
}

上記の図が示すように、解決策は疑似要素::view-transition-old::view-transition-newに対してobject-fit: coverを適用することだ。これにより、スナップショット画像がアスペクト比を維持したまま切り抜かれるようになる。

疑似要素ツリーの構造を理解する

ビュー遷移が発生すると、ブラウザは内部的に次のような疑似要素ツリーを生成する。

::view-transition
└── ::view-transition-group(hero-img)
    ├── ::view-transition-old(hero-img)
    └── ::view-transition-new(hero-img)

::view-transition-groupが古い寸法から新しい寸法へアニメーションするコンテナとして機能し、その中のoldnewが実際のスナップショットを保持する。デフォルトではこれらの疑似要素にobject-fit: fillが適用されており、これが歪みの原因となる。

アスペクト比が大きく異なるケースでは、object-positionで切り抜き位置を調整することも有効だ。

::view-transition-old(hero-img) {
  object-fit: cover;
  object-position: center center;
}
::view-transition-new(hero-img) {
  object-fit: cover;
  object-position: center top;
}

このコードでは、新しいヒーロー画像の上部を優先的に表示しつつ、遷移中の歪みを防ぐことができる。CSS-Tricksの著者も指摘するように、object-fit: coverはほぼ全ての画像遷移で必要になる設定であり、デフォルトがfillであることは実用上の大きな障壁となっている。

pageswapとpagerevealによるライフサイクル制御

pageswapとpagerevealによるライフサイクル制御

クロスドキュメントビュー遷移では、遷移元と遷移先のページがJavaScriptで直接通信できないという制約がある。この問題を解決するのがpageswappagerevealの2つのイベントだ。

遷移元ページ pageswap発火 要素に名前を付与
event.activation.entry.url で遷移先を特定可能
遷移先ページ pagereveal発火 遷移元の情報を取得
navigation.activation.from.url で遷移元を特定可能

このイベントペアにより、開発者は遷移の両端で状態を制御できる。pageswapは遷移元ページがスナップショットされる直前に発火し、event.activation.entry.urlでユーザーがどこへ向かっているかを知ることができる。

イベントハンドラの実装パターン

これらのイベントを使用する際の重要なポイントは、必ずevent.viewTransitionの存在確認を行うことだ。pagerevealはビュー遷移がない場合も含め、全てのナビゲーションで発火する。

window.addEventListener("pagereveal", (event) => {
  if (!event.viewTransition) return;
  
  event.viewTransition.finished.then(() => {
    // 遷移完了後のクリーンアップ
  }).catch((err) => {
    // タイムアウト等のエラー処理
  });
});

CSS-Tricksの記事では、商品一覧ページから商品詳細ページへの遷移において、pageswapでクリックされた商品カードだけにview-transition-nameを動的に付与するパターンが紹介されている。この動的な名前付けは、数十から数百の要素があるページでのスケーラビリティ問題を解決する重要な手法だ。

この記事のポイント

  • metaタグ方式は非推奨。CSSの@view-transition { navigation: auto; }を使用する
  • 4秒のタイムアウトはTTFBを含む総時間で判定され、pagerevealイベントで捕捉可能
  • 画像歪みは疑似要素::view-transition-old/newへのobject-fit: cover適用で解決
  • pageswappagerevealの2つのイベントが遷移全体のライフサイクルを制御する
View Transitions APIでサイト体験を劇的に変える!CSSだけで実現する7つのページ遷移レシピ

View Transitions APIでサイト体験を劇的に変える!CSSだけで実現する7つのページ遷移レシピ

Webサイトのページを切り替える際、画面が瞬時にパッと切り替わるのではなく、モバイルアプリのような滑らかなアニメーションを伴う手法が注目されている。これを実現するのが「View Transitions API」だ。複雑なJavaScriptライブラリを使わずに、ブラウザの標準機能とCSSだけで高度な遷移エフェクトを実装できる。

View Transitions APIは主要なブラウザでのサポートが進み、実用的な段階に入った。特にマルチページアプリケーション(MPA)でも、ページ間の連続性を保った演出が可能になった点は大きい。ユーザー体験を向上させるための強力な武器になるだろう。

本記事では、CSS-Tricksの記事を基に、すぐに試せる7つのアニメーションレシピを紹介する。基本的なセットアップから、ぼかしや3D回転を組み合わせた応用例まで、その仕組みを詳しく解説していく。技術的なハードルは低いため、最新のWeb制作トレンドを取り入れたいエンジニアやデザイナーにとって有益な情報となるはずだ。

View Transitions APIの基本設定と導入のポイント

View Transitions APIの基本設定と導入のポイント

View Transitions APIを利用するには、まずブラウザに対して「このサイトでページ遷移のアニメーションを有効にする」という宣言が必要だ。これを「オプトイン(利用選択)」と呼ぶ。CSSの @view-transition アットルールを使い、遷移元と遷移先の両方のページで設定を行う必要がある。

@view-transitionルールでのオプトイン

最も基本的な設定は、CSSに数行のコードを追加するだけで完了する。共通のCSSファイルに記述しておくことで、サイト全体に適用できる。 navigation: auto を指定すると、通常のリンク移動時にブラウザが自動的にトランジションを実行するようになる。

@view-transition {
  navigation: auto;
}

この設定だけで、ブラウザのデフォルトである「クロスフェード(前の画面が消えながら次の画面が浮き上がる)」が適用される。さらに特定の名前(タイプ)を付けることで、ページの種類ごとに異なるアニメーションを使い分けることも可能だ。

ユーザーの好みに配慮したアクセシビリティ対応

アニメーションを実装する上で忘れてはならないのが、アクセシビリティへの配慮だ。OSの設定で「視覚効果を減らす」を選択しているユーザーに対しては、激しい動きを控えるべきだ。これを判定するのが prefers-reduced-motion メディアクエリである。

「動きを減らす」設定が無効(no-preference)の場合のみアニメーションを有効にする記述が推奨される。これにより、すべてのユーザーが快適にサイトを閲覧できる環境を整えられる。技術的な新しさを追求するだけでなく、こうした配慮をセットで行うのがプロの仕事だ。

@media (prefers-reduced-motion: no-preference) {
  @view-transition {
    navigation: auto;
    types: my-transition;
  }
}

視覚効果で魅せるフェードとワイプのレシピ

視覚効果で魅せるフェードとワイプのレシピ

ここからは具体的なアニメーションレシピを見ていこう。まずは定番のフェード効果をアレンジしたものや、画面を拭き取るようなワイプ効果だ。これらは汎用性が高く、どんなジャンルのサイトにも馴染みやすい。

ぼかしを活用したPixelate dissolve

単なるフェードではなく、画面全体をぼかしながら切り替えるのが「Pixelate dissolve(ピクセレート・ディゾルブ)」だ。CSSの filter: blur() プロパティを使用する。古いページがぼやけて消えていき、新しいページがぼやけた状態から鮮明に現れる演出だ。

t=0% (開始・古いページ)
元のコンテンツ(鮮明)
t=50% (ぼかしながらクロスフェード中)
遷移中(ぼやけて溶け合う)
t=100% (完了・新しいページ)
新しいコンテンツ(鮮明)

このアニメーションは実際には約1.4秒かけて連続的に行われる。中間状態の filter:blur(6px)opacity:0.6 がスムーズに変化することで、画面全体が一度ぼやけてから鮮明な新画面が立ち上がる演出になる。短く設定すればキビキビとしたモダンな操作感、長くすればゆったりとした高級感のある印象を与えられる。

clip-pathで実現する上下左右のWipe効果

「Wipe(ワイプ)」は、画面をスライドさせて覆い隠すような効果だ。これには clip-path プロパティの inset() 関数を利用する。 inset() は要素の表示領域を上下左右からの距離で指定する仕組みで、この数値を 0% から 100% へ動かすことで、コンテンツを削り取るような動きを作れる。

例えば「Wipe up(上方向へのワイプ)」なら、古いページの表示領域を下から上へ 100% 削り、新しいページを上から下へ 0% に戻していく。 clip-path を使うメリットは、実際のレイアウトを崩さずに表示領域だけを制御できる点にある。非常にパフォーマンスが良く、滑らかな動きを実現できる。

ダイナミックな動きを作る回転とプッシュの演出

ダイナミックな動きを作る回転とプッシュの演出

次に、より動きの大きいダイナミックな演出を紹介する。これらはユーザーの目を引きやすいため、ポートフォリオサイトやキャンペーンページなど、個性を出したい場面で有効だ。 transform プロパティを駆使して、空間的な広がりを演出する。

遊び心のあるRotate in-out

「Rotate in-out(回転イン・アウト)」は、ページが回転しながら縮小して消え、新しいページが逆回転しながら拡大して現れるエフェクトだ。 scale(0)rotate(180deg) を組み合わせる。実用性は限られるかもしれないが、View Transitionsの表現力の高さを示す良い例だ。

t=0% (開始・元ページ)
元コンテンツ
t=50% (切替の瞬間・縮小して回転中)
回転中
t=100% (完了・新ページ)
新コンテンツ

このアニメーションは実際には約1秒かけて連続的に行われる。中間状態では transform:scale(0.3) rotate(90deg)opacity:0.4 によって元ページが縮小しながら回転して消え、その直後に新ページが逆方向から拡大して現れる。transform-origin: center を指定して画面中央を軸に回転させるのがポイントだ。また、回転角度を大きくしすぎるとユーザーが酔ってしまう可能性があるため、180度程度に抑えておくのが無難だ。

画面の隅から現れるDiagonal push

「Diagonal push(斜めプッシュ)」は、古いページを斜め方向に押し出し、新しいページを逆の斜め方向から滑り込ませる演出だ。 translate(-100%, -100%) のように X軸 と Y軸 の両方を同時に動かすことで斜めの移動を実現する。

この演出は、スライド資料を切り替えるような感覚をユーザーに与える。移動の軌跡に合わせて opacity (不透明度)を変化させると、より自然で洗練された印象になる。 ease (緩急)の指定を工夫することで、重厚感のある動きから軽快な動きまで調整可能だ。

形状と奥行きを活かした高度なトランジション

形状と奥行きを活かした高度なトランジション

最後に、より高度な視覚効果を紹介する。これらは clip-path の応用や 3D変形 を使用しており、実装には少しコツが必要だが、その分インパクトは非常に大きい。ブラウザが自動的に生成するスナップショットをどのように加工するかが鍵となる。

円形に広がるCircle wipe-out

「Circle wipe-out(サークル・ワイプ)」は、画面中央から円形に新しいページが広がっていく演出だ。映画のシーン切り替えなどで見かける手法である。 clip-path: circle() を使い、半径を 0% から 150% まで拡大させることで、画面全体を覆い尽くす動きを作る。

このレシピの面白い点は、背景色が同じページ間での遷移だ。背景が変わらずにコンテンツだけが円形に浮き上がってくるように見えるため、非常にシームレスな体験を提供できる。中心点は at 50% 50% だけでなく、クリックした位置に合わせて動的に変更するような応用も考えられる。

幕が開くようなCurtain reveal

「Curtain reveal(カーテン・リビール)」は、舞台の幕が左右に開くような動きだ。これも clip-path: inset() を使用するが、左右の値を 50% から 0% へと変化させる点が特徴だ。画面中央から左右に向かって新しいページが露出していく様子は、新しい体験の始まりを予感させる。

t=0% (幕が完全に閉じている)
舞台の中身
t=50% (幕が左右に開いて中身が半分見える)
舞台の中身
t=100% (幕が完全に開いて全体が見える)
舞台の中身がフル表示
幕(左右から覆う部分)  新ページのコンテンツ

このアニメーションは実際には約0.8秒かけて連続的に行われる。clip-path: inset(0 50% 0 50%) から inset(0 0 0 0) へと値が変化することで、左右から幕が引かれて中央のコンテンツが露出していく。実際のView Transitionsでは、::view-transition-new(root) に対してこのクリッピングアニメーションを適用することで、滑らかなカーテン効果が実現する。

3D空間でカードがめくれる3D flip

最もインパクトがあるのが「3D flip(3Dフリップ)」だ。ページ全体を一枚のカードに見立て、 Y軸 を中心に回転させて裏返すような演出を行う。 rotateY(90deg) でページを真横に向け、その瞬間に新しいページと入れ替えて 0deg に戻していく。

この演出を成功させるには、 perspective (遠近感)の設定が重要だ。奥行きを感じさせる数値を指定することで、平面的な画面の中に立体的な空間が生まれる。ただし、非常に目立つエフェクトなので、使いどころを慎重に選ぶ必要があるだろう。

実務でView Transitionsを導入する際の注意点

実務でView Transitionsを導入する際の注意点

View Transitions APIは非常に強力だが、実務に導入する際にはいくつか考慮すべき点がある。単にコードをコピーするだけでなく、プロジェクトの要件に合わせた最適化が必要だ。ここでは、技術的な側面とユーザー体験の両面から、筆者の見解を交えて解説する。

ブラウザサポートとフォールバックの考え方

View Transitions APIは現在、ChromeやEdgeなどのChromium系ブラウザで先行して実装され、SafariやFirefoxでも順次対応が進んでいる。しかし、すべてのユーザーが最新ブラウザを使っているわけではない。そのため、「アニメーションが動かなくてもコンテンツは正しく表示される」というプログレッシブ・エンハンスメントの考え方が不可欠だ。

幸いなことに、View Transitions APIは「対応していないブラウザでは単にアニメーションが無視されるだけ」という特性を持っている。特別なJavaScriptによる条件分岐を書かなくても、基本的には安全に導入できる。ただし、アニメーションがあることを前提とした複雑なUI設計は避けるべきだ。

パフォーマンスへの影響と最適化

トランジション実行中、ブラウザは画面のスナップショット(画像のようなもの)を作成し、それをアニメーションさせている。そのため、非常に高解像度な画像が大量にあるページや、複雑なDOM構造を持つページでは、一瞬の動作の重さを感じることがあるかもしれない。

対策としては、 will-change プロパティを適切に使ってブラウザに最適化を促すことや、アニメーションさせる要素を view-transition-name で限定することが有効だ。画面全体(root)を動かすのではなく、ヘッダーやロゴなどの共通要素を固定し、中身のコンテンツだけを動かすようにすると、より軽快で自然な遷移になる。

この記事のポイント

  • View Transitions APIはCSSだけでモバイルアプリのような滑らかなページ遷移を実現する
  • @view-transition ルールの navigation: auto 設定でMPAでも簡単に導入できる
  • clip-pathfilter を組み合わせることで、ぼかしやワイプなど多様な演出が可能になる
  • prefers-reduced-motion を使い、動きを好まないユーザーへの配慮を忘れない
  • 対応ブラウザ以外では通常の遷移になるため、プログレッシブ・エンハンスメントとして導入しやすい